Swift Package Registry Service - Security

Last month, I submitted a pitch for a Swift package registry service, and the feedback we received from the community was excellent. (Thank you all so much!) We’re working to incorporate that feedback into a revised draft soon.

In particular, I’d like to thank @lukasa for his feedback about the proposed use of digital signatures for package verification. Because security is a core value proposition of this proposal, I wanted to spin up a new thread to focus on that specifically (as suggested by @rballard).

I’m going to start by sharing what I’ve learned since posting the original pitch last month, and will then describe some ideas I had for alternative approaches to security. My hope is that these ideas can serve as a good starting point for a discussion on this topic.


The story so far

A primary goal of the proposed registry service is to provide strong guarantees that the package you downloaded is authentic. One approach is built on trust: If you assume that a registry always sends you exactly what you ask for, you only need to verify the sender (though it wouldn’t hurt to verify the contents anyway).

Modern information security relies on public-key cryptography to verify claims of identity. Broadly speaking, there are two approaches to certificate trust:

The original proposal relies on both TLS and PGP for security; TLS to verify the identity of the registry’s domain (e.g. github.com) and PGP to verify the registry as the creator of the package archive. My thinking was that this “belt and suspenders” approach would offer more security than relying on one alone. Instead, this turned out to be more of a “weak link in the chain”.

“PGP considered harmful”

A few days after I pitched this on the forums, Filippo Valsorda, who’s is in charge of cryptography and security on the Go team at Google, reached out to caution against using PGP. Although a decentralized “web of trust” model can work in theory, PGP itself has some serious issues in practice. An article titled “The PGP Problem” provides a good summary of these issues.

A quick aside to help clarify any confusion around PGP naming:

  • PGP (“Pretty Good Privacy”): A piece of software developed by Phil Zimmermann in 1991.
  • OpenPGP: An open standard for PGP’s functionality, codified by IETF RFC 4880
  • GPG (GnuPG or “Gnu Privacy Guard”): A popular piece of software that implements the OpenPGP standard.

To its credit, PGP remains a popular security standard, and in many cases, it’s better than nothing. For most systems, “pretty good” is good enough. For example, I’ll continue to sign my Git commits with GPG (until GitHub adds support for something better). However, there appears to be a consensus among security researchers that new systems can do better by choosing a modern, focused tool for their security needs.

“We don’t use the ‘B’ word” (Blockchain)

I had the privilege of chatting with Filippo and his colleague, Katie Hockman, who explained the approach they took with Go’s checksum database. The basic idea is that, when a new version of a module is published, a checksum of its contents is added to a public, append-only log.

Fun Fact: The terms “package” and “module” in Swift have the opposite meanings in Go.

This approach is very different from the one initially proposed for the package registry. Instead of trusting that a registry is on the up-and-up, a transparent log allows you to be inherently distrustful. Because the entire, immutable history of package release is made public, it can be independently audited and verified that a checksum for a release hasn’t changed.

Adopting this approach, we could do away with the /mona/LinkedList/1.0.0.zip.asc endpoint entirely. Instead, the response for the Zip archive could provide the checksum for the package release contents in a header (distinct from Digest which is for the Zip file itself;Content-Checksum, perhaps?). The checksum can then be verified against the registry’s checksum database. Counter-intuitively, this simpler approach offers stronger guarantees than a digital signature, which can only attest to the identity of who archived a package, not what’s inside.

Client-side, the security model is Trust on first use (TOFU). When you add a 3rd-party dependency to your own Swift project, SPM would download the package, verify its integrity based on the registry-provided checksum, and then store that checksum in a new field in Package.resolved.

// Straw-man proposal
{
  "object": {
    "pins": [
      {
        "package": "LinkedList",
        "url": "https://swift.pkg.github.com/mona/LinkedList",
        "state": {
          "checksum": ["sha256", "ed008d5af44c1d0ea0e3668033cae9b695235f18b1a99240b7cf0f3d9559a30d"],
          "version": "1.2.0"
        }
      }
    ]
  },
  "version": 2
}

Later, when your CI downloads those dependencies during a build, it can use the checksum in the checked-in Package.resolved file to verify that the contents of the package haven't changed for that version. TOFU operates under the principle that "you can't fool everyone all of the time". There's a chance that you download a forged package the first time you resolve a new dependency, but it will likely be caught the next time you or someone else attempts to validate the checksum of the forged package. (TUF, discussed later on, offers stronger guarantees than TOFU)

Note: Transparency logs and digital signatures are complementary. If digital signatures were something we wanted to support, there’s no technical reason why we couldn’t have a checksum database and signatures (either using OpenPGP or something like signify or minisign).

Go’s checksum database is powered by a piece of software called Trillian, which is written an Go and sponsored by Google. Filippo noted that there’s interest to make Trillian available for deployment in other language ecosystems, so that may be an option for Swift.

Seeing the Forest for the (Merkle) Trees

At the heart of this append-only log are Merkle Trees. If you’ve heard of them before, there’s a good chance it was in the context of Bitcoin (though Katie was quick to point out that Merkle Trees ≭ blockchain).

But you know what else uses Merkle Trees? Git.

So I got to thinking, “Could we use Git as a transparency log?” It’s not as far-out as you might think. Rustaceans do this for Crates.io. CocoaPods actually does this, too.

I started to sketch out how this might work with a simple Bash script that runs through the steps of adding a new package release.

$ tree .
.
└── 2e
    └── 80
        └── 6e
            └── 827387c39e8c594053f5c90701dbb5b5aa00a83812cb0452b5695e45be
                ├── 1.2.0.zip
                └── 1.2.0.zip.asc

4 directories, 3 files

# Use git tree to lookup releases for a package (according to content-addressable path)
$ git ls-files "$(checksum "$package")" -x "*.zip"
2e/80/6e/827387c39e8c594053f5c90701dbb5b5aa00a83812cb0452b5695e45be/1.2.0.zip

# Verify that release archive hasn't been modified
$ git log --follow "$(checksum "$package")/1.2.0.zip" --oneline
792276c (HEAD -> master) sha-256=c584882ef498cc6e043e0f100134483dfa04ea2d5921f56

Some highlights of this proof-of-concept:

  • Using Git provides a familiar, trustworthy mechanism for auditing integrity (it’s one thing to technically secure; it’s another to have consensus, understanding, and trust that it is secure)
  • Using SHA-256 for content-addressable mapping with packages shards packages evenly in filesystem, protects against homographic attacks, and provides a possible mechanism for privacy / anonymity
  • Using Git notes allows changes to metadata without affecting history of the release artifact
  • Using Git LFS keeps repository slim and storage agnostic

I’d be interested to hear any thoughts you have— especially as it compares to, for example, Go’s checksum database.

Securing the Software Supply Chain

In the course of my research, I learned about The Update Framework (TUF), as well as its sister project In-Toto. Together, these frameworks can go a long way to improving package and supply chain security.

As I understand it, transparent logs and TUF provide different guarantees and can be implemented together. This article by Trishank Karthik Kuppusamy and Marina Moore has a great explanation of these two approaches and how they relate.

I’m still looking into how TUF and TLs can fit together in the context of the package registry proposal. So if you have any ideas about what that might look like, I’d love to hear them.


To summarize:

  • There are practical reasons to avoid OpenPGP in a new specification
  • A checksum database / transparent log is a simple and effective strategy for securing the package ecosystem that doesn't (directly) require public-key cryptography
  • Frameworks like TUF can provide even stronger security guarantees
22 Likes

Thanks for starting this new thread @mattt, I think it's definitely worth pressing on this issue to get something in really great shape. I'm also thrilled to see that Filippo and Katie have reached out to chat about what's going on in their own community: both Filippo and Katie are well qualified to talk about this issue and their opinions are extremely valuable.

I'm much happier about the idea of a transparency log as a baseline bootstrap for package authenticity than PGP. Transparency logs have lots of prior art behind them and are used across the industry. They can be federated, allowing the possibility that other package registries can also maintain transparency logs and that those entries can be cross-checked. They're also exceedingly simple to implement: indeed, Swift Crypto already has all the pieces required to build such a log.

The idea of using Git as the baseline for implementing the transparency log is also interesting and worth investigating. We should carefully consider the tradeoffs, and whether adding Git as a dependency here provides more value than it costs in complexity. For my part (with my cryptography engineer hat on) I'll say that I suspect using Git for this adds more complexity than we need, but I'm certainly open to being talked around on the matter.

As for TUF, I know much less about TUF than I do about transparency logs. I'm vaguely aware of it from discussions with Donald Stufft about PyPI, which has been interested in TUF for a long time. However, I don't think they've made forward progress on it.

I do think that if we felt that a transparency log provided insufficient guarantees, we should investigate TUF as the next logical step in preference to PGP. I'll do some more reading on TUF and try to form a more educated opinion.

7 Likes

It was a pleasure talking to you about this and this is an excellent summary. I'm happy to answer any questions about the Go Checksum Database if they come up.

A quick note about the trust model: transparent logs are not simply TOFU on the client side, like SSH is, but are TOFU ecosystem-wide, which is a much stronger property. When the transparent log operator first fetches a specific release, they trust whatever fetching mechanism they use (like an HTTPS endpoint); then, they permanently and uniquely include the checksum in the log and no one will need to trust the fetching mechanism again. (Instead, they rely on the append-only properties of the log, proven by the inclusion and consistency proofs, and verified by the auditors.)

Katie gave a very good explanation in the second half of her talk about the Go modules infrastructure and we touch upon it in the Go proposal that introduced the Checksum Database.

A core difference between transparent logs and TUF is that the latter only delivers stronger security if it's deployed in "PEP 480" mode (the Secure Systems Lab article you linked does a good job explaining the difference), where individual software authors have to manage their own keys, which is a significant security and usability hurdle.

5 Likes

I'm so glad to hear that, @lukasa. Thanks again for pushing me to reconsider what was, admittedly, a "color by numbers" decision on my part, adopting what I understood at the time to be a "best practice" (but turned out to be more of an anti-pattern).

Talking to Filippo and Katie and security folks in GitHub has definitely been a highlight of this project so far. I'm very thankful for their generosity and willingness to help solve these important problems of supply chain security.

What I like about Git is its ubiquity. Most Swift developers use Git everyday, and this familiarity imparts a kind of essential obviousness. Many of us know first-hand what happens when someone else force pushes to master / main to rewrite history. A custom solution may be better, both operationally and cryptographically, but it would have to build trust and consensus from scratch.

By way of analogy, it's like a magician walking into a Walmart and buying a sealed deck of cards before doing a card trick, or walking into a Walmart and purchasing a cheap laptop to be used in a cryptocurrency ceremony, or voting on a paper ballot instead of a machine (though this comparison would be quite uncharitable, given the proven insecurity of electronic voting, at least in America).

One last note about Git, because this isn't clear from my first post: For the purposes of the registry specification, any decision to use Git as a transparency log would be an implementation detail rather than a requirement. I'd want to abstract this through an API like how the Go checksum does with log tiles.

The complexity of TUF is my biggest concern. I know of at least a few language ecosystems, including RubyGems have struggled to implement it. Speaking personally, I'm still not 100% on what a deployment looks like in practice. So yeah, please share your notes as you look into TUF — it'd be great to get a better handle on everything.


@FiloSottile Thanks again for helping out with this! Your and Katie's reputation, and of the Go team, generally, bring immense confidence and excitement to this process.

Oh, interesting! That's indeed an important distinction.

Just to make sure I'm following correctly, would this property hold under Swift's slightly different model? Whereas Go (as I understand it) creates new releases the first time they're requested, our proposed system requires authors to publish releases to a registry before others can pull them in as dependencies. Here's the scenario I'd imagined:

  • Mona publishes mona/LinkedList version 1.2.1 to GitHub.com
  • The registry adds the checksum for mona/LinkedList 1.2.1 to its transparent log
  • Alice is MitM'd by Mallory, who intercepts all HTTPS traffic to to Github.com — including package registry endpoints and the transparent log repository
  • Alice installs mona/LinkedList the first time
  • Mallory sends a forged package and checksum to Alice
  • Alice commits the forged checksum in Package.resolved
  • Bob pulls Alice's changes and tries to install mona/LinkedList, but gets a checksum mismatch error

If Mallory could also attack Carol and David in the same way, would we be able to claim that stronger stronger TOFU property? (Apologies if I'm missing something here. I'm sure I'll figure it out after rewatching Katie's talk you linked to)

That does sound challenging. Are there any deployments of TUF that you'd point to as good examples of what to do? I was especially interested to see if anyone was taking the approach that article describes under "Getting the best of both worlds".

Note that Git is more like a blockchain than a transparent log in that you have to download the whole history to ensure an entry is included, while Merkle Trees have compact verifiable inclusion proofs.

BTW, my recommendation with my cryptography engineer hat on is to abstract nothing, and specify exactly what everything does and how it works. Joints and abstractions is where cryptography breaks in practice.

Good question. The answer is a bit subtle.

First, in this design you are delegating authentication of the transparent log entirely to GitHub. In Go the registry makes a signature on the tree head, which has several advantages:

  1. Mallory needs to compromise the registry key to MitM Alice, which is why our transparent log is as secure as TUF in "PEP 458" mode;
  2. the transparent log can be served by untrusted proxies, because the client will check the signature from the Google key;
  3. auditors can prove that the registry misbehaved if they find something wrong in the transparent log, like two different checksums for the same release.

(3) is especially important, because in your current model the registry operator could say "the auditor is lying, we never served that log" and no one would have any way to verify who's saying the truth.

Note that this is another thing you'd have to hack on top of git if you wanted to use it as a backend, maybe with tag signing (which is kind of a UX and crypto nightmare).

Even without the tree head signature, though, there is recourse for Alice that doesn't go through Package.resolved: first, since you only ever extend the tree (in git, you only ever fast-forward), the attacker can only serve a completely fake tree the first time Alice fetches it (in git, when she clones it; ideally only once in her life), and after that can only append checksums (risking making the tree inconsistent if a checksum for the same release was already logged); second, Alice can and should (ideally automatically) compare the tree head (in git, the commit hash) with others to ensure everyone is on the same tree. This is the concept of gossiping, and it's what various ecosystems are working on solving in different ways, but can be done most easily by letting clients send their tree heads directly to auditors (and again for this to work well you need signatures, so when Alice goes to the auditor with a tree head that is not part of the mainline tree, the auditor can confidently accuse the registry of lying).

I'm not the most up to date person on TUF deployments, but I'm not aware of anyone who deployed a hybrid with a transparent log, nor of any large ecosystem that pulled off a "PEP 480" style deployment.

3 Likes

You’re very welcome: I want the Swift community to have a really great package registry available to it, and I’m invested in us working to get a design we can all feel happy with. An individual engineer cannot be expected to be an expert in everything, which is why doing this collaboratively is so important!

I think this is a very important note. There are some real advantages to deploying a strictly Merkle-tree based approach. Essentially all language ecosystems are equipped to handle both building them and verifying inclusion (Swift Crypto has the necessary pieces). They are, as cryptography goes, not terribly complex. While the analogy to familiar tooling is definitely useful, we may be able to provide something “just-as-good” by providing a small set of extremely-user-friendly tools to manage working with the transparency log.

I think the gossiping approach works best if we can get SwiftPM (or other associated tooling) to download the log to a central location, rather than doing so on a per-repository basis. That allows users to establish their logs earlier and persist them longer.

Presumably we can get an even better ecosystem-wide TOFU by encouraging the deployment of transparency log mirrors. For example, enterprises that are particularly concerned about this problem could mirror the transparency log locally and participate as one of the gossiping peers. Some mirrors could even be deployed publicly.

3 Likes