Respin: upspin revival

#bazel#go#upspin

tl;dr: I revived the upspin project source code. See it at https://github.com/filmil/upspin, and https://github.com/filmil/upspin-gdrive. Since April, it has also gained post-quantum encryption, streaming of large files, a writable keyserver, and a lot of automated checking. Read on to learn what that actually means.

Updated on 2026-10-10. The first version of this article described the revival as of April 2026. This version adds what changed since, in What happened since and How it is checked now, and updates the list of what is still missing.

History

Upspin was a project intended to provide a global namespace for all digital artifacts. It ended up mostly being used as a distributed file store, although the idea was more general than that. It was quite useful even as storage. And as far as distributed filesystems go, it was by far the simplest portable way to share storage between different machines.

For all its utility, it saw relatively little uptake. In February 2025 the authors announced the turndown of its infrastructure. The main piece was the Upspin keyserver at key.upspin.io, which stored the public keys of all Upspin users. Those keys are what lets Upspin keep data encrypted at rest.

All of Upspin quietly assumed that this keyserver was always reachable. So when it was turned down, Upspin installations everywhere stopped working.

This nicely illustrated the distributed computing adage attributed to Leslie Lamport:

A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.

The other thing that stopped working was a way to get up to date binaries.

It’s gone, what to do?

I had a somewhat involved data setup that used Upspin backed by Google Drive. Upspin was the most convenient way to share data among my computers, and I wanted to keep using it. There were no satisfactory replacements. The official Google Drive client doesn’t work on Linux, and the unofficial ones are slow and unreliable. Dropbox and the like are also spotty. “Eternal” stores like Perkeep seem to have a huge initial setup cost.

In theory, a simple federated keyserver is easy: change the code to look up the key of user foo@example.com at a keyserver at key.example.com. In practice, key.upspin.io was baked deep into the Upspin libraries. Changing that needed detailed knowledge of the code base, and that takes time to get.

AI coding assistants made finding my way around the code base much easier. That suggested the time was ripe to revive the project, so that my existing data could be reused.

What I did in April

I revived the project to the point where an existing Upspin user can deploy the new binaries and get to their existing data. Full functionality is possible too, as long as you run your own keyserver. I did not need that, because I never ended up sharing data with others. But if you are the kind of person who bothers with Upspin, you may well want to take that work up yourself.

I am almost sure that the original Upspin authors would find some choice words1 for my changes to their code and infrastructure. I didn’t ask anyone’s opinion before doing this. But it was something I could do, and do well, and the result is something I can maintain.

  • I moved the build infrastructure to GitHub. The original authors preferred mailing lists and patches. I’m soft and weak, and I like GitHub.

  • I moved the build system to Bazel. Maintaining the code means more than building Go binaries. I am one person, against 53 contributors to the original project, so I can’t build out every workflow a project like this needs. Bazel lets me reuse existing rules to build binaries, containers and deployment recipes for cloud VMs. All of that can be done while keeping Go builds pure. But I don’t work on this for pay, and I have a day job, so I cut the toil where I can. To all who are ticked off by this: I’m sorry. I wrote “why I bother with Bazel” to explain a bit.

  • I added scaffolding for AI coding assistants. Some Upspin authors have been famously vocal against AI. But my time is limited, and an assistant lets me go further, and faster. I offloaded plenty of tasks to Gemini CLI2, and to Jules on the Web.

  • I wrote a “local” keyserver, with Gemini’s help. It is a keyserver you host on your own infrastructure. I had made earlier attempts, but I hadn’t caught all the places where key.upspin.io was hardcoded, which made for a very confusing deployment.

  • I changed the Upspin libraries to use your own keyserver. The key of user@domain.com is now looked up at key.domain.com, instead of the defunct key.upspin.io.

  • I fixed the Google Drive store server’s OAuth setup. It used Google’s out-of-band OAuth flow, which Google has retired. It now uses the loopback flow: the setup command starts a local web server, opens the browser and picks up the authorization by itself. You bring your own Google OAuth client: upspin-setupstorage-drive reads its ID and secret from the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables, and stores both in the server’s configuration, so that the server can refresh its access later.

  • I set up GitHub workflows that build binary releases and containers regularly, and run the tests on every change.

What happened since

The April revival kept Upspin alive. Since then I kept going, and these are the larger changes. They are in releases 45.0.0 and later. Most of this later work was done with Claude Code. The fork’s own feature list is in README.filmil.md.

Post-quantum encryption

When the original authors announced the turndown, they listed improvements that contributors had proposed and that were waiting for review. One of them was “post-quantum-cryptographic packing”. That is now in the fork, as a new packing called eepq.

The threat is “harvest now, decrypt later”. Someone stores your encrypted files today, and decrypts them once a quantum computer can break elliptic curve cryptography. Upspin’s ee packing encrypts each file with its own AES-256 key. It then wraps that key for each reader with elliptic curve Diffie-Hellman. The wrapping is the weak point against a quantum computer.

eepq keeps everything else in ee: the AES-256 data encryption, the checksums and the ECDSA signatures. It changes only the key wrapping. The wrapping key is derived from two shared secrets: one from elliptic curve Diffie-Hellman, and one from ML-KEM, the post-quantum key exchange standardized as FIPS 203. The two are combined with HKDF, in the style of X-Wing. An attacker has to break both to get the file key.

Using it takes a few steps:

  • It is opt-in. Every binary that should handle it, servers included, needs the -eepq flag, or experimental: eepq in its config.
  • Each user needs a post-quantum key. The key types pair a curve with an ML-KEM parameter set of matching strength: p256+mlkem768, p384+mlkem768 or p521+mlkem1024. upspin keygen -rotate -curve p256+mlkem768 rotates to one.
  • packing: eepq in the config makes new files use it. Every reader of a file must also have a post-quantum key, otherwise writing or sharing the file stops with an error that names the reader.
  • requirepacking: eepq makes the client refuse to read or write regular files under any other packing. That stops an attacker from downgrading a file to the weaker packing.

It has costs. A directory entry grows from about 263 bytes to about 1,353 bytes for one reader, and each further reader adds about 1,251 bytes. A post-quantum public key also has four lines, where older binaries expect three. So upgrade every binary in a deployment before anyone rotates to a post-quantum key. Files written earlier under ee are not changed. If they must be protected, write them again under eepq.

Large files

Upspin used to hold a whole file in memory while writing it. The FUSE mount, upspinfs, read the entire cached file into one buffer, with a comment in the code that said “Hope it fits”. So a file larger than the machine’s memory could not be stored at all, even though the store server only ever handles one block at a time.

Writes now stream. The client packs and stores one block at a time, and spools anything beyond one block to a temporary file on local disk. upspinfs and upspin put both stream. The blocks and directory entries come out exactly as before. The client library gained two methods for this, which is a breaking change for programs that implement its interface.

A writable keyserver

The local keyserver started out read-only, serving keys from a JSON file. Given an -out file, it now also accepts key updates. It saves them to that file, and loads them on top of the original keys on the next start. The original file is never changed. The keyserver also ships in the release container.

Releases and containers

  • Releases happen only when something changed since the last one. Each is a tagged GitHub release, with binaries, and with a provenance attestation for the source archive. The attestation is a signed record of which workflow built the archive from which commit.
  • Containers for Linux on amd64 and arm64 are published with every release, under its version number. A deployment can pin the exact version it runs, instead of a floating latest.
  • Bazel modules. Every release is published to my own Bazel registry, for both upspin and upspin-gdrive. Both are also checked the way the Bazel Central Registry would check them. A separate test module depends on upspin as an outside user would, and is built on Linux and macOS with Bazel 8 and 9.

How it is checked now

Adding post-quantum code to an encryption system is the kind of change that should make you nervous. The eepq packing was reviewed four times, adversarially, by an AI assistant. Each review produced a list of work items, and most of them became checks that now run on every pull request:

  • Static analysis: go vet, staticcheck and gosec. govulncheck looks for known vulnerabilities in the dependencies, and gitleaks looks for committed secrets.
  • The race detector runs over the encryption, key handling and client packages.
  • Fuzzing: every parser has a fuzz target. Each pull request fuzzes every target for 60 seconds. A nightly run fuzzes each for ten minutes, and adds what it finds to the committed corpus, so each night starts from the last one’s discoveries. The key parsers also reject oversized key material before parsing it.
  • Golden files: the exact bytes written by ee, eepq and the other packings are pinned in test data, including entries made by the code from before the fork. A change that alters the wire format fails the tests.
  • Tampering: a matrix of tests changes each part of an encrypted entry, for each kind of reader and each operation, and checks that every change is caught. Other tests check that one kind of key or packing cannot be passed off as another, and that each wrapping uses fresh keys.
  • Old and new binaries: for every pull request, CI builds the upspin binary both from the target branch and from the change. It starts a small Upspin cluster, and checks that each binary reads files the other wrote, for a small file and a three megabyte one. That is how I know the new code still reads data written by the old.
  • Coverage floors: the encryption and key handling packages have a minimum line coverage, which may only go up.

None of this makes Upspin’s storage guarantees better than they were. It does make it much less likely that a change of mine quietly breaks the format of data already stored.

What I still didn’t do

Please bear in mind that the revival is still simplistic. It provides stable fundamentals for the long-term availability of the binaries and infrastructure. It does not provide user friendliness. If you have use cases and good will to change that, send patches.

  • No human cryptographer has reviewed eepq. All four reviews were done by an AI assistant. The code is in shape for an expert to read in an afternoon, but nothing replaces that reading. Until it happens, treat eepq as experimental, as its flag says.
  • No general keyserver. The local keyserver serves keys that you already have, and the updates you send it. It is not a signup service, so upspin setupserver for a brand new user still won’t work out of the box.
  • No verified keyserver access. The traffic should be served over HTTPS with current certificates. All other cryptographic properties are reused from what was already there.
  • No better storage guarantees. Upspin is and was alpha quality, and you should not store on it anything you can’t afford to lose. While making the first changes, I destroyed all my Upspin data twice. I still don’t know exactly why.

Conclusion

Upspin works again, but for now only if you are a former user and you have all your keys on hand.

In April, I had something that appeared to work on my machine. Since then it has learned to store files of any size and to protect new files against a future quantum computer. Every change is now checked against the data it has to keep reading. All unit tests pass, except the original end-to-end tests, which relied on infrastructure that was never public.

The main gap, in case you see a chance to get something useful for yourself, is that I haven’t made this easy for others to use. If you want that, fix the remaining issues. Send patches.

In the end, my contribution here is still small. I automated something that basically anyone could have. I used AI coding assistants heavily, first to find my way around a code base that had long escaped my understanding, and then to do much of the work. I probably ruined some of the aesthetics of the original project in the process. For that, I apologize.


  1. Some of them went on an adventure to design and build out an entire new programming language and ecosystem, after dealing with the complexities of Google’s build system. ↩︎

  2. Why Gemini and not, say, Claude? In April, Gemini seemed to fare much better with Bazel than Claude did, and I had learned to wrangle it. The later work was done mostly with Claude. ↩︎