Signing packages during the build


#1

Several package types support signing, for example, UEFI kernel images and Android packages. Signing requires a private key during the build. While the private key has to remain secret, the signed result itself could be public.

In this case, the private key is needed during the build, which is a different use case than the key distribution feature of NixOps, which is intended for keys used during the runtime. If I understood correctly, currently nixpkgs builds do not sign kernel images, for example.

Is there a way to produce signed binaries using Nix?


#2

This all basically depends on having private files in the Nix store.


#3

Actually I’m not really sure having private files in the Nix store would
be enough for this use case: another issue is that (almost) any user on
the system is allowed to build things via Nix, so the private key would
also have to be:

  • Unlockable by a nix build
  • But only from the user owning the key
  • And without introducing a dependency towards the key, so that’d also
    require a change to the sandbox

#4

In the context of POSIX, would it not work if the private key in the store had mode 0440, is owned by root and has group privatefile_userA. An ACL allows read access to userA separately, or the user could be a member of privatefile_userA.

  • userA can access the file directly.
  • If the nix builder would be called by userA, the builder process would have privatefile_userA as a supplementary group. Therefore, the builder can access the file during the build.
  • A more sophisticated implementation would allow different security labels, and the builder would get access to files needed by the build, if the caller has access to those security labels. In POSIX, each security label could correspond to a group.

The private key file would not be referenced by the build result, so the result would not become a referrer of the private key. The private key would still be an input of the derivation, so it would affect the hash of the output. I don’t see a problem with that: a binary signed with a different private key is a different output.


#5

As for supporting private files in the nix store, I think a sane default would be to derive the permissions from the source. If the source file is accessible only by the owner, so should be the store file. The same would apply to files written by nix scripts: if the script is accessible only by the owner, so should be any written store files.

Of course, a script should be able mark a result as public, even if the script is not, or use any other permissions allowed by the system.

This would require introducing the concept of the origin. When the nix evaluator reads a script, it would label functions with the origin. When the script would call toFile etc., nix would check the origin of the caller.


#6

I think if we wanted to do signed builds currently, the way to do it would like like this:

  • The private key exists inside an isolated container. The build is done inside the container.
  • The key is fetched by a fixed-output derivation.
    • Inside the container, the private key can be fetched, and the build succeeds.
    • Outside the container, fetching the key would always fail. However, the fixed-output derivation has the same name and the same hash.
  • A signed package built inside the container can be imported to a nix store outside the container.

#7

This sounds like a possibility, indeed :slight_smile: though it’d be a bit painful to handle. For the ACLs on the store, there have been numerous discussions, and currently consensus appears (to me) to be “well, (almost) everything can be handled with encryption, and given it’d be really hard to handle full-scale ACLs, let’s just do that.”

But I’d encourage commenting on https://github.com/NixOS/rfcs/pull/5 so that maybe one day it can be merged and we can start exploring the limits of what it offers.


#8

At the moment I would treat the signature operation as an impure operation that happens during the publication time. The CI would basically do something like this:

# build
$ out=$(nix-build --no-out-path -A my-package)
# sign
$ gpg --sign "$out"
# publish
$ aws s3 cp "$out" *.gpg s3://my-bucket/releases

The biggest downside is that composability is lost


#9

In a lot of real-world scenarios, your private key won’t exist as bits on a disk to which a general-purpose OS (much less the general-purpose OS running on a build server) has access at all.

It may be escrowed in hardware. It may be owned by an access-controlled signing server, which only accepts requests associated with time- and usage-count-limited tickets that are issued after humans do paperwork. Etc.

As someone who deals with build systems that involve (multiple layers of) signing steps on a day-to-day basis, I would consider the ideal solution one that treats both the content to be signed and the identity, but not the content, of the key used as inputs. If content-with-hash-A has been signed by a key with identifier B, we can reuse that signature if we ever get content-with-hash-A again, without actually needing to see the key material ourselves.

Which is all to say that a code-signing mechanism engineered with a presumption that private key material exists on the build server at all, much less exists in the Nix store, is substantially limiting its scope-of-use.