GnuPG 2.4.1 was released all the way back in May but I failed to notice until I upgraded to NixOS 23.11 a few weeks ago. And I only noticed because Emacs froze when I tried to save an encrypted file. It didn’t take me long to realize it was due to a change in GnuPG.

Emacs and Encrypted Files

When working with encrypted files Emacs goes to great lengths to ensure the unencrypted cleartext is never written to the file system. It does this by invoking GnuPG directly and communicating with it using pipes. Thus, to write to an encrypted file, Emacs sends the cleartext down the pipe and GnuPG writes the encrypted contents to a file.

Just before Emacs sends the cleartext down the pipe to GnuPG it waits for GnuPG to signal that it’s ready to read cleartext. The problem with GnuPG (versions 2.4.1 through 2.4.3) is that it sends this signal after it reads from standard input. In other words, both Emacs and GnuPG are waiting for one another at the same time, resulting in a classic I/O deadlock.

The Workaround

The Emacs maintainers consider this situation a bug in GnuPG. And after a few days the GnuPG maintainers fixed the bug. It will be part of GnuPG 2.4.4 release. Until then the solution is to downgrade GnuPG. The Emacs PROBLEMS file says:

The only known workaround is to downgrade to a version of GnuPG older than 2.4.1 (or, in the future, upgrade to a newer version which solves the problem, when such a fixed version becomes available).

Pinning Packages in nixpkgs

This presents the perfect opportunity to demonstrate several ways that nixpkgs ecosystem allows us to solve the problem of downgrading a package, sometimes called package pinning.

The easiest solution is to use an overlay, a function that takes the current package set and returns a new package set. The overlay below replaces the gnupg package with one pinned at version 2.4.0:

final: prev:

{
  gnupg = prev.gnupg.overrideAttrs (orig: {
    version = "2.4.0";
    src = prev.fetchurl {
      url = "mirror://gnupg/gnupg/gnupg-2.4.0.tar.bz2";
      hash = "sha256-HXkVjdAdmSQx3S4/rLif2slxJ/iXhOosthDGAPsMFIM=";
    };
  });
}

Notice that the overlay function actually takes two arguments. The second argument (prev) is the previous package set, the one we want to alter. Since overlays can be chained together we may be manipulating the original package set or one from a previous overlay.

Sometimes we need access to the package set the way it will be after all overlays have been applied. For example, wanting to use a dependency from a later overlay in the current overlay. In this case the first argument (final) comes in handy, it’s the final package set after all overlays have been applied. (This is possible thanks to Nix being a lazy language and the use of fixpoints.)

Livin’ La Vida Gentoo

Unfortunately, using an overlay to pin GnuPG comes at a cost. It turns out that GnuPG is a dependency in a lot of packages. This means that after applying the overlay, nixpkgs will determine that all of those packages need to be rebuilt, and we won’t be able to take advantage of the binary cache (because the cache was built with GnuPG 2.4.1).

I don’t have the patience of a Gentoo user and compiling a ton of packages from source isn’t my idea of a good time. Besides, it’s a terrible use of the power grid. Thankfully, we have other options.

The NixOS Module

If you use NixOS then the easiest way to install and configure GnuPG is via the NixOS module:

{
  programs.gnupg.enable = true;
}

The NixOS module also has a package option so users can override the GnuPG package. Taking the overlay code from above we can write our own NixOS module and load it into our system configuration (via the normal imports list mechanism):

{ pkgs }:

{
  programs.gnupg.package = pkgs.gnupg.overrideAttrs (orig: {
    version = "2.4.0";
    src = pkgs.fetchurl {
      url = "mirror://gnupg/gnupg/gnupg-2.4.0.tar.bz2";
      hash = "sha256-HXkVjdAdmSQx3S4/rLif2slxJ/iXhOosthDGAPsMFIM=";
    };
  });
}

Similar to the overlay method, pinning GnuPG through the NixOS module affects the version of GnuPG in the system PATH. But unlike with overlays, other packages still see the original version of GnuPG. Since we’re not infecting other packages with a changed GnuPG, nothing needs to be recompiled.

Now, I’m a very happy user of NixOS, but this isn’t my preferred solution. And, of course, it doesn’t work at all for non-NixOS users. The last way we’ll look at pinning GnuPG works for any nixpkgs user and also doesn’t suffer from the overhead of the overlay technique.

The Home Manager Module

I’m a huge fan of Home Manager. It allows nixpkgs users take all the awesomeness of NixOS to other Linux distributions and even to closed operating systems like macOS, Android, and even Windows.

The Home Manager module for GnuPG is very similar to the NixOS module and also includes a package option. That means we can pin GnuPG in our user environment similar to how the NixOS module allowed us to pin it at the system level. The following Nix code can be loaded into your Home Manager configuration through the normal imports list mechanism, just like with NixOS modules:

{ pkgs }:

{
  programs.gpg.package = pkgs.gnupg.overrideAttrs (orig: {
    version = "2.4.0";
    src = pkgs.fetchurl {
      url = "mirror://gnupg/gnupg/gnupg-2.4.0.tar.bz2";
      hash = "sha256-HXkVjdAdmSQx3S4/rLif2slxJ/iXhOosthDGAPsMFIM=";
    };
  });
}

A Warning About Other Workarounds

It’s important to point out that the correct solution to this problem between Emacs and GnuPG is to fix GnuPG.

I’ve seen people on social media sites like Reddit suggesting a hack to Emacs to resolve the problem. Specifically, people are putting the following in their Emacs configuration:

(fset 'epg-wait-for-status 'ignore)

Don’t do this! This is not a valid fix and may lead to file corruption when reading/writing encrypted files.

This bit of hackery replaces the epg-wait-for-status function with one that ignores its arguments and always returns nil. That function is used throughout the EPG library in Emacs, not just when writing files through GnuPG. Again, please don’t do this.

Conclusion

We’ve looked at three different ways of pinning a package in nixpkgs:

  1. Using an overlay function.

    This is usually my go-to solution. But in this case GnuPG is used in so many other packages that it caused massive rebuilds.

  2. Pinning via the NixOS programs.gnupg.package option.

    This works by only changing the version of GnuPG installed in the system PATH. If you use NixOS (and not Home Manager) this is the way to go.

  3. Pinning via the Home Manager programs.gpg.package option.

    This is my preferred solution because it works nearly everywhere thanks to Home Manager. It replaces the version of GnuPG in your user PATH, leaving the system PATH and the rest of nixpkgs alone.

Like many others, my frustration with GnuPG is nearing its breaking point. Over the last 20 years I’ve sent/received no more than a handful of encrypted emails. My use of GnuPG at this point is limited to encrypting files in my password store and signing Git commits. It’s becoming less clear why I put up with the burden of managing my GnuPG key and the occasional upgrade problem.

Perhaps I’ll finally switch to age over the next major holiday break. (I probably won’t find the time, but here’s hoping for the best.)


Update (January 6, 2024): A better solution may be to patch the current version of GnuPG so it doesn’t have the deadlock bug. I explore that option in this post.

Update (February 23, 2024): GnuPG 2.4.4 has been released and fixes the problems mentioned in this post.