Due to the use of hash maps, the order of file installation was not
deterministic. I've changed the code the use BTreeMaps instead, which
makes this deterministic. While I was here, I tried to simplify the
code a bit.
To minimize writes to the ESP but still find necessary changes, compare
the hashes of the files on the ESP with the "expected" hashes. Only copy
and overwrite already existing files if the hashes don't match. This
ensures a working-as-expected state on the ESP as opposed to previously
where already existing files were just ignored.
Previously, generations were installed one after another. Now all
artifacts (kernels, initrd etc.) are first collected and then installed.
This way the writes to the ESP are reduced as duplicate paths are
already removed in the collection phase.
Using random names for tempfiles makes handling them easier. It reduces
the amount of noise in the code because no custom name needs to be
provided for each tempfile. The names were not really useful in any
case.
It also does not burden the developer with ensuring uniqueness of names.
This is relevant when files for multiple generations need to be stored
in the same directory (e.g. because they need to be accessed after
handling one generation).
Out of an abundance of caution, 32 random alphanumeric characters are
chosen for each filename. The tempfile crate, in comparison, only
chooses 8. 32 characters should be enough to avoid collisions, even
if the PRNG is not of cryptographic quality.
Leverage the bootspec `label` field in its intended way. The VERSION_ID
of the os-release in the stub now only contains the generation number
and the build time. This makes a correct PRETTY_NAME entirely dependent
on correct information in the bootspec `label` field.
Read the build time from generation symlinks in /nix/var/nix/profiles
instead of from the underlying derivation. The derivation build time
will always be a UNIX epoch of 0 because of the `nix-build` sandbox,
which is useless for identifying when a generation was created.
Do not pass our own cmdline on to the kernel. It may have been set by a
malicious boot loader specification entry, and could instruct the
kernel to load an arbitrary unprotected initrd (or perform some other
fun stuff). Instead, always pass the command line built into the UKI,
which is properly authenticated.
Malicious boot loader specification entries could be used to make a
signed kernel load arbitrary unprotected initrds. Since we do not want
this, do not sign the kernel. This way, the only things allowed to boot
are our UKI stubs, which do verify the initrd.
When loading something with UEFI LoadImage, signature validation is
performed. However, we verify the kernel by its hash already, and don't
want to sign it. Hence, we have to load it on our own.
To minimize the number of arguments passed to `lzbt`, the loader config
is assembled outside `lzbt` and passed as a single argument.
Instead of reimplementing `consoleMode` under the `lanzaboote`
namespace, `config.loader.systemd-boot.consoleMode` is reused as is.