diff --git a/TODO b/TODO index 0c87d15e10..af41aa57ac 100644 --- a/TODO +++ b/TODO @@ -4,11 +4,6 @@ Bugfixes: manager or system manager can be always set. It would be better to reject them when parsing config. -* busctl --user call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager GetUnitProcesses "s" run-rbff1b85427b34ba3adf864281aeda8e7.service -Failed to set address: No such file or directory - - → improve error message - External: * Fedora: add an rpmlint check that verifies that all unit files in the RPM are listed in %systemd_post macros. @@ -42,7 +37,14 @@ Features: that are linked to these places instead of copied. After all they are constant vendor data. -* seed: check if first-boot and then don't do anything +* maybe add kernel cmdline params: 1) to force first-boot mode + 2) to force + random seed crediting + +* nspawn: on cgroupsv1 issue cgroup empty handler process based on host events, + so that we make cgroup agent logic safe + +* nspawn/machined: add API to invoke binary in container, then use that as + fallback in "machinectl shell" * logind: rework pam_logind to also do a bus call in case of invocation from user@.service, which returns the XDG_RUNTIME_DIR value, and make this diff --git a/docs/BOOT_LOADER_INTERFACE.md b/docs/BOOT_LOADER_INTERFACE.md index 50488ee97a..cbd66876af 100644 --- a/docs/BOOT_LOADER_INTERFACE.md +++ b/docs/BOOT_LOADER_INTERFACE.md @@ -70,6 +70,28 @@ variables. All EFI variables use the vendor UUID * `1 << 2` → The boot loader honours `LoaderEntryDefault` when set. * `1 << 3` → The boot loader honours `LoaderEntryOneShot` when set. * `1 << 4` → The boot loader supports boot counting as described in [Automatic Boot Assessment](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT). + * `1 << 5` → The boot loader supports looking for boot menu entries in the Extended Boot Loader Partition. + * `1 << 6` → The boot loader spports passing a random seed to the OS. + +* The EFI variable `LoaderRandomSeed` contains a binary random seed if set. It + is set by the boot loader to pass an entropy seed read from the ESP partition + to the OS. The system manager then credits this seed to the kernel's entropy + pool. It is the responsibility of the boot loader to ensure the quality and + integrity of the random seed. + +* The EFI variable `LoaderSystemToken` contains binary random data, + persistently set by the OS installer. Boot loaders that support passing + random seeds to the OS should use this data and combine it with the random + seed file read from the ESP. By combining this random data with the random + seed read off the disk before generating a seed to pass to the OS and a new + seed to store in the ESP the boot loader can protect itself from situations + where "golden" OS images that include a random seed are replicated and used + on multiple systems. Since the EFI variable storage is usually independent + (i.e. in physical NVRAM) of the ESP file system storage, and only the latter + is part of "golden" OS images, this ensures that different systems still come + up with different random seeds. Note that the `LoaderSystemToken` is + generally only written once, by the OS installer, and is usually not touched + after that. If `LoaderTimeInitUSec` and `LoaderTimeExecUSec` are set, `systemd-analyze` will include them in its boot-time analysis. If `LoaderDevicePartUUID` is set, @@ -77,7 +99,9 @@ systemd will mount the ESP that was used for the boot to `/boot`, but only if that directory is empty, and only if no other file systems are mounted there. The `systemctl reboot --boot-loader-entry=…` and `systemctl reboot --boot-loader-menu=…` commands rely on the `LoaderFeatures` , -`LoaderConfigTimeoutOneShot`, `LoaderEntries`, `LoaderEntryOneShot` variables. +`LoaderConfigTimeoutOneShot`, `LoaderEntries`, `LoaderEntryOneShot` +variables. `LoaderRandomSeed` is read by PID during early boot and credited to +the kernel's random pool. ## Boot Loader Entry Identifiers diff --git a/docs/RANDOM_SEEDS.md b/docs/RANDOM_SEEDS.md new file mode 100644 index 0000000000..7edf7c2d6a --- /dev/null +++ b/docs/RANDOM_SEEDS.md @@ -0,0 +1,418 @@ +--- +title: Random Seeds +--- + +# Random Seeds + +systemd can help in a number of ways with providing reliable, high quality +random numbers from early boot on. + +## Linux Kernel Entropy Pool + +Today's computer systems require random number generators for numerous +cryptographic and other purposes. On Linux systems, the kernel's entropy pool +is typically used as high-quality source of random numbers. The kernel's +entropy pool combines various entropy inputs together, mixes them and provides +an API to userspace as well as to internal kernel subsystems to retrieve +it. This entropy pool needs to be initialized with a minimal level of entropy +before it can provide high quality, cryptographic random numbers to +applications. Until the entropy pool is fully initialized application requests +for high-quality random numbers cannot be fulfilled. + +The Linux kernel provides three relevant userspace APIs to request random data +from the kernel's entropy pool: + +* The [`getrandom()`](http://man7.org/linux/man-pages/man2/getrandom.2.html) + system call with its `flags` parameter set to 0. If invoked the calling + program will synchronously block until the random pool is fully initialized + and the requested bytes can be provided. + +* The `getrandom()` system call with its `flags` parameter set to + `GRND_NONBLOCK`. If invoked the request for random bytes will fail if the + pool is not initialized yet. + +* Reading from the + [`/dev/urandom`](http://man7.org/linux/man-pages/man4/urandom.4.html) + pseudo-device will always return random bytes immediately, even if the pool + is not initialized. The provided random bytes will be of low quality in this + case however. Moreover the kernel will log about all programs using this + interface in this state, and which thus potentially rely on an uninitialized + entropy pool. + +(Strictly speaking there are more APIs, for example `/dev/random`, but these +should not be used by almost any application and hence aren't mentioned here.) + +Note that the time it takes to initialize the random pool may differ between +systems. If local hardware random number generators are available, +initialization is likely quick, but particularly in embedded and virtualized +environments available entropy is small and thus random pool initialization +might take a long time (up to tens of minutes!). + +Modern hardware tends to come with a number of hardware random number +generators (hwrng), that may be used to relatively quickly fill up the entropy +pool. Specifically: + +* All recent Intel and AMD CPUs provide the CPU opcode + [RDRAND](https://en.wikipedia.org/wiki/RdRand) to acquire random bytes. Linux + includes random bytes generated this way in its entropy pool, but didn't use + to credit entropy for it (i.e. data from this source wasn't considered good + enough to consider the entropy pool properly filled even though it was + used). This has changed recently however, and most big distributions have + turned on the `CONFIG_RANDOM_TRUST_CPU=y` kernel compile time option. This + means systems with CPUs supporting this opcode will be able to very quickly + reach the "pool filled" state. + +* The TPM security chip that is available on all modern desktop systems has a + hwrng. It is also fed into the entropy pool, but generally not credited + entropy. You may use `rng_core.default_quality=1000` on the kernel command + line to change that, but note that this is a global setting affect all + hwrngs. (Yeah, that's weird.) + +* Many Intel and AMD chipsets have hwrng chips. Their Linux drivers usually + don't credit entropy. (But there's `rng_core.default_quality=1000`, see + above.) + +* Various embedded boards have hwrng chips. Some drivers automatically credit + entropy, others do not. Some WiFi chips appear to have hwrng sources too, and + they usually do not credit entropy for them. + +* `virtio-rng` is used in virtualized environments and retrieves random data + from the VM host. It credits full entropy. + +* The EFI firmware typically provides a RNG API. When transitioning from UEFI + to kernel mode Linux will query some random data through it, and feed it into + the pool, but not credit entropy to it. What kind of random source is behind + the EFI RNG API is often not entirely clear, but it hopefully is some kind of + hardware source. + +If neither of these are available (in fact, even if they are), Linux generates +entropy from various non-hwrng sources in various subsystems, all of which +ultimately are rooted in IRQ noise, a very "slow" source of entropy, in +particular in virtualized environments. + +## `systemd`'s Use of Random Numbers + +systemd is responsible for bringing up the OS. It generally runs as the first +userspace process the kernel invokes. Because of that it runs at a time where +the entropy pool is typically not yet initialized, and thus requests to acquire +random bytes will either be delayed, will fail or result in a noisy kernel log +message (see above). + +Various other components run during early boot that require random bytes. For +example, initial RAM disks nowadays communicate with encrypted networks or +access encrypted storage which might need random numbers. systemd itself +requires random numbers as well, including for the following uses: + +* systemd assigns 'invocation' UUIDs to all services it invokes that uniquely + identify each invocation. This is useful retain a global handle on a specific + service invocation and relate it to other data. For example, log data + collected by the journal usually includes the invocation UUID and thus the + runtime context the service manager maintains can be neatly matched up with + the log data a specific service invocation generated. systemd also + initializes `/etc/machine-id` with a randomized UUID. (systemd also makes use + of the randomized "boot id" the kernel exposes in + `/proc/sys/kernel/random/boot_id`). These UUIDs are exclusively Type 4 UUIDs, + i.e. randomly generated ones. + +* systemd maintains various hash tables internally. In order to harden them + against [collision + attacks](https://rt.perl.org/Public/Bug/Display.html?CSRF_Token=165691af9ddaa95f653402f1b68de728) + they are seeded with random numbers. + +* At various places systemd needs random bytes for temporary file name + generation, UID allocation randomization, and similar. + +* systemd-resolved and systemd-networkd use random number generators to harden + the protocols they implement against packet forgery. + +* systemd-udevd and systemd-nspawn can generate randomized MAC addresses for + network devices. + +Note that these cases generally do not require a cryptographic-grade random +number generator, as most of these utilize random numbers to minimize risk of +collision and not to generate secret key material. However, they usually do +require "medium-grade" random data. For example: systemd's hash-maps are +reseeded if they grow beyond certain thresholds (and thus collisions are more +likely). This means they are generally fine with low-quality (even constant) +random numbers initially as long as they get better with time, so that +collision attacks are eventually thwarted as better, non-guessable seeds are +acquired. + +## Keeping `systemd'`s Demand on the Kernel Entropy Pool Minimal + +Since most of systemd's own use of random numbers do not require +cryptographic-grade RNGs, it tries to avoid reading entropy from the kernel +entropy pool if possible. If it succeeds this has the benefit that there's no +need to delay the early boot process until entropy is available, and noisy +kernel log messages about early reading from `/dev/urandom` are avoided +too. Specifically: + +1. When generating [Type 4 + UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_\(random\)), + systemd tries to use Intel's and AMD's RDRAND CPU opcode directly, if + available. While some doubt the quality and trustworthiness of the entropy + provided by these opcodes, they should be good enough for generating UUIDs, + if not key material (though, as mentioned, today's big distributions opted + to trust it for that too, now, see above — but we are not going to make that + decision for you, and for anything key material related will only use the + kernel's entropy pool). If RDRAND is not available or doesn't work, it will + use synchronous `getrandom()` as fallback, and `/dev/urandom` on old kernels + where that system call doesn't exist yet. This means on non-Intel/AMD + systems UUID generation will block on kernel entropy initialization. + +2. For seeding hash tables, and all the other similar purposes systemd first + tries RDRAND, and if that's not available will try to use asynchronous + `getrandom()` (if the kernel doesn't support this system call, + `/dev/urandom` is used). This may fail too in case the pool is not + initialized yet, in which case it will fall back to glibc's internal rand() + calls, i.e. weak pseudo-random numbers. This should make sure we use good + random bytes if we can, but neither delay boot nor trigger noisy kernel log + messages during early boot for these use-cases. + +## `systemd`'s Support for Filling the Kernel Entropy Pool + +systemd has various provisions to ensure the kernel entropy is filled during +boot, in order to ensure the entropy pool is filled up quickly. + +1. When systemd's PID 1 detects it runs in a virtualized environment providing + the `virtio-rng` interface it will load the necessary kernel modules to make + use of it during earliest boot, if possible — much earlier than regular + kernel module loading done by `systemd-udevd.service`. This should ensure + that in VM environments the entropy pool is quickly filled, even before + systemd invokes the first service process — as long as the VM environment + provides virtualized RNG hardware (and VM environments really should!). + +2. The + [`systemd-random-seed.service`](https://www.freedesktop.org/software/systemd/man/systemd-random-seed.service.html) + system service will load a random seed from `/var/lib/systemd/random-seed` + into the kernel entropy pool. By default it does not credit entropy for it + though, since the seed is — more often than not — not reset when 'golden' + master images of an OS are created, and thus replicated into every + installation. If OS image builders carefully reset the random seed file + before generating the image it should be safe to credit entropy, which can + be enabled by setting the `$SYSTEMD_RANDOM_SEED` environment variable for + the service to `1`. Note however, that this service typically runs + relatively late during early boot: long after the initial RAM disk + (`initrd`) completed, and after the `/var/` file system became + writable. This is usually too late for many applications, it is hence not + advised to rely exclusively on this functionality to seed the kernel's + entropy pool. Also note that this service synchronously waits until the + kernel's entropy pool is initialized before completing start-up. It may thus + be used by other services as synchronization point to order against, if they + require an initialized entropy pool to operate correctly. + +3. The + [`systemd-boot`](https://www.freedesktop.org/software/systemd/man/systemd-boot.html) + EFI boot loader included in systemd is able to maintain and provide a random + seed stored in the EFI System Partition (ESP) to the booted OS, which allows + booting up with a fully initialized entropy pool from earliest boot + on. During installation of the boot loader (or when invoking [`bootctl + random-seed`](https://www.freedesktop.org/software/systemd/man/bootctl.html#random-seed)) + a seed file with an initial seed is placed in a file `/loader/random-seed` + in the ESP. In addition, an identically sized randomized EFI variable called + the the 'system token' is set, which is written to the machine's firmware + NVRAM. During boot, when `systemd-boot` finds both the random seed file and + the system token they are combined and hashed with SHA256 (in counter mode, + to generate sufficient data), to generate a new random seed file to store in + the ESP as well as a random seed to pass to the OS kernel. The new random + seed file for the ESP is then written to the ESP, ensuring this is completed + before the OS is invoked. Very early during initialization PID 1 will read + the random seed provided in the EFI variable and credit it fully to the + kernel's entropy pool. + + This mechanism is able to safely provide an initialized entropy pool already + in the `initrd` and guarantees that different seeds are passed from the boot + loader to the OS on every boot (in a way that does not allow regeneration of + an old seed file from a new seed file). Moreover, when an OS image is + replicated between multiple images and the random seed is not reset, this + will still result in different random seeds being passed to the OS, as the + per-machine 'system token' is specific to the physical host, and not + included in OS disk images. If the 'system token' is properly initialized + and kept sufficiently secret it should not be possible to regenerate the + entropy pool of different machines, even if this seed is the only source of + entropy. + + Note that the writes to the ESP needed to maintain the random seed should be + minimal. The size of the random seed file is directly derived from the Linux + kernel's entropy pool size, which defaults to 512 bytes. This means updating + the random seed in the ESP should be doable safely with a single sector + write (since hard-disk sectors typically happen to be 512 bytes long, too), + which should be safe even with FAT file system drivers built into + low-quality EFI firmwares. + + As a special restriction: in virtualized environments PID 1 will refrain + from using this mechanism, for safety reasons. This is because on VM + environments the EFI variable space and the disk space is generally not + maintained physically separate (for example, `qemu` in EFI mode stores the + variables in the ESP itself). The robustness towards sloppy OS image + generation is the main purpose of maintaining the 'system token' however, + and if the EFI variable storage is not kept physically separate from the OS + image there's no point in it. That said, OS builders that know that they are + not going to replicate the built image on multiple systems may opt to turn + off the 'system token' concept by setting `random-seed-mode always` in the + ESP's + [`/loader/loader.conf`](https://www.freedesktop.org/software/systemd/man/loader.conf.html) + file. If done, `systemd-boot` will use the random seed file even if no + system token is found in EFI variables. + +With the three mechanisms described above it should be possible to provide +early-boot entropy in most cases. Specifically: + +1. On EFI systems, `systemd-boot`'s random seed logic should make sure good + entropy is available during earliest boot — as long as `systemd-boot` is + used as boot loader, and outside of virtualized environments. + +2. On virtualized systems, the early `virtio-rng` hookup should ensure entropy + is available early on — as long as the VM environment provides virtualized + RNG devices, which they really should all do in 2019. Complain to your + hosting provider if they don't. + +3. On Intel/AMD systems systemd's own reliance on the kernel entropy pool is + minimal (as RDRAND is used on those for UUID generation). This only works if + the CPU has RDRAND of course, which most physical CPUs do (but I hear many + virtualized CPUs do not. Pity.) + +4. In all other cases, `systemd-random-seed.service` will help a bit, but — as + mentioned — is too late to help with early boot. + +This primarily leaves two kind of systems in the cold: + +1. Some embedded systems. Many embedded chipsets have hwrng functionality these + days. Consider using them while crediting + entropy. (i.e. `rng_core.default_quality=1000` on the kernel command line is + your friend). Or accept that the system might take a bit longer to + boot. Alternatively, consider implementing a solution similar to + systemd-boot's random seed concept in your platform's boot loader. + +2. Virtualized environments that lack both virtio-rng and RDRAND. Tough + luck. Talk to your hosting provider, and ask them to fix this. + +3. Also note: if you deploy an image without any random seed and/or without + installing any 'system token' in an EFI variable, as described above, this + means that on the first boot no seed can be passed to the OS + either. However, as the boot completes (with entropy acquired elsewhere), + systemd will automatically install both a random seed in the GPT and a + 'system token' in the EFI variable space, so that any future boots will have + entropy from earliest boot on — all provided `systemd-boot` is used. + +## Frequently Asked Questions + +1. *Why don't you just use getrandom()? That's all you need!* + + Did you read any of the above? getrandom() is hooked to the kernel entropy + pool, and during early boot it's not going to be filled yet, very likely. We + do use it in many cases, but not in all. Please read the above again! + +2. *Why don't you use + [getentropy()](http://man7.org/linux/man-pages/man3/getentropy.3.html)? That's + all you need!* + + Same story. That call is just a different name for `getrandom()` with + `flags` set to zero, and some additional limitations, and thus it also needs + the kernel's entropy pool to be initialized, which is the whole problem we + are trying to address here. + +3. *Why don't you generate your UUIDs with + [`uuidd`](http://man7.org/linux/man-pages/man8/uuidd.8.html)? That's all you + need!* + + First of all, that's a system service, i.e. something that runs as "payload" + of systemd, long after systemd is already up and hence can't provide us + UUIDs during earliest boot yet. Don't forget: to assign the invocation UUID + for the `uuidd.service` start we already need a UUID that the service is + supposed to provide us. More importantly though, `uuidd` needs state/a random + seed/a MAC address/host ID to operate, all of which are not available during + early boot. + +4. *Why don't you generate your UUIDs with `/proc/sys/kernel/random/uuid`? + That's all you need!* + + This is just a different, more limited interface to `/dev/urandom`. It gains + us nothing. + +5. *Why don't you use [`rngd`](https://github.com/nhorman/rng-tools), + [`haveged`](http://www.issihosts.com/haveged/), + [`egd`](http://egd.sourceforge.net/)? That's all you need!* + + Like `uuidd` above these are system services, hence come too late for our + use-case. In addition much of what `rngd` provides appears to be equivalent + to `CONFIG_RANDOM_TRUST_CPU=y` or `rng_core.default_quality=1000`, except + being more complex and involving userspace. These services partly measure + system behavior (such as scheduling effects) which the kernel either + already feeds into its pool anyway (and thus shouldn't be fed into it a + second time, crediting entropy for it a second time) or is at least + something the kernel could much better do on its own. Hence, if what these + daemons do is still desirable today, this would be much better implemented + in kernel (which would be very welcome of course, but wouldn't really help + us here in our specific problem, see above). + +6. *Why don't you use [`arc4random()`](https://man.openbsd.org/arc4random.3)? + That's all you need!* + + This doesn't solve the issue, since it requires a nonce to start from, and + it gets that from `getrandom()`, and thus we have to wait for random pool + initialization the same way as calling `getrandom()` + directly. `arc4random()` is nothing more than optimization, in fact it + implements similar algorithms that the kernel entropy pool implements + anyway, hence besides being able to provide random bytes with higher + throughput there's little it gets us over just using `getrandom()`. Also, + it's not supported by glibc. And as long as that's the case we are not keen + on using it, as we'd have to maintain that on our own, and we don't want to + maintain our own cryptographic primitives if we don't have to. Since + systemd's uses are not performance relevant (besides the pool initialization + delay, which this doesn't solve), there's hence little benefit for us to + call these functions. That said, if glibc learns these APIs one day, we'll + certainly make use of them where appropriate. + +7. *This is boring: NetBSD had [boot loader entropy seed + support](https://netbsd.gw.com/cgi-bin/man-cgi?boot+8) since ages!* + + Yes, NetBSD has that, and the above is inspired by that (note though: this + article is about a lot more than that). NetBSD's support is not really safe, + since it neither updates the random seed before using it, nor has any + safeguards against replicating the same disk image with its random seed on + multiple machines (which the 'system token' mentioned above is supposed to + address). This means reuse of the same random seed by the boot loader is + much more likely. + +8. *Why does PID 1 upload the boot loader provided random seed into kernel + instead of kernel doing that on its own?* + + That's a good question. Ideally the kernel would do that on its own, and we + wouldn't have to involve userspace in this. + +9. *What about non-EFI?* + + The boot loader random seed logic described above uses EFI variables to pass + the seed from the boot loader to the OS. Other systems might have similar + functionality though, and it shouldn't be too hard to implement something + similar for them. Ideally, we'd have an official way to pass such a seed as + part of the `struct boot_params` from the boot loader to the kernel, but + this is currently not available. + +10. *I use a different boot loader than `systemd-boot`, I'd like to use boot + loader random seeds too!* + + Well, consider just switching to `systemd-boot`, it's worth it. See + [systemd-boot(7)](https://www.freedesktop.org/software/systemd/man/systemd-boot.html) + for an introduction why. That said, any boot loader can re-implement the + logic described above, and can pass a random seed that systemd as PID 1 + will then upload into the kernel's entropy pool. For details see the [Boot + Loader Interface](https://systemd.io/BOOT_LOADER_INTERFACE) documentation. + +11. *Why not pass the boot loader random seed via kernel command line instead + of as EFI variable?* + + The kernel command line is accessible to unprivileged processes via + `/proc/cmdline`. It's not desirable if unprivileged processes can use this + information to possibly gain too much information about the current state + of the kernel's entropy pool. + +12. *Why doesn't `systemd-boot` rewrite the 'system token' too each time + when updating the random seed file stored in the ESP?* + + The system token is stored as persistent EFI variable, i.e. in some form of + NVRAM. These memory chips tend be of low quality in many machines, and + hence we shouldn't write them too often. Writing them once during + installation should generally be OK, but rewriting them on every single + boot would probably wear the chip out too much, and we shouldn't risk that. diff --git a/man/bootctl.xml b/man/bootctl.xml index 46b9738b14..822d07a606 100644 --- a/man/bootctl.xml +++ b/man/bootctl.xml @@ -45,15 +45,15 @@ Path to the EFI System Partition (ESP). If not specified, /efi/, - /boot/, and /boot/efi are checked in turn. It is recommended to mount - the ESP to /efi/, if possible. + /boot/, and /boot/efi/ are checked in turn. It is + recommended to mount the ESP to /efi/, if possible. Path to the Extended Boot Loader partition, as defined in the Boot Loader Specification. If not - specified, /boot/ are checked. It is recommended to mount the Extended Boot + specified, /boot/ is checked. It is recommended to mount the Extended Boot Loader partition to /boot/, if possible. @@ -70,8 +70,17 @@ This option modifies the behaviour of status. Only prints the path to the Extended Boot Loader partition if it exists, and the path to the ESP otherwise to standard output and exit. This command is useful to determine where to place boot loader entries, as they are - preferably placed in the Extended Boot Loader partition if it exists and in the ESP otherwise. - + preferably placed in the Extended Boot Loader partition if it exists and in the ESP otherwise. + + Boot Loader Specification Type #1 entries should generally be placed in the directory + $(bootctl -x)/loader/entries/. Existence of that directory may also be used as + indication that boot loader entry support is available on the system. Similarly, Boot Loader + Specification Type #2 entries should be placed in the directory $(bootctl + -x)/EFI/Linux/. + + Note that this option (similar to the option mentioned + above), is available independently from the boot loader used, i.e. also without + systemd-boot being installed. @@ -124,6 +133,31 @@ and the firmware's boot loader list. + + + + Generates a random seed and stores it in the EFI System Partition, for use by the + systemd-boot boot loader. Also, generates a random 'system token' and stores it + persistently as an EFI variable, if one has not been set before. If the boot loader finds the random + seed in the ESP and the system token in the EFI variable it will derive a random seed to pass to the + OS and a new seed to store in the ESP from the combination of both. The random seed passed to the OS + is credited to the kernel's entropy pool by the system manager during early boot, and permits + userspace to boot up with an entropy pool fully initialized very early on. Also see + systemd-boot-system-token.service8. + + See Random Seeds for further + information. + + + + + + Checks whether systemd-boot is installed in the ESP. Note that a + single ESP might host multiple boot loaders; this hence checks whether + systemd-boot is one (of possibly many) installed boot loaders — and neither + whether it is the default nor whether it is registered in any EFI variables. + + @@ -165,7 +199,8 @@ systemd-boot7, Boot Loader Specification, - Boot Loader Interface + Boot Loader Interface, + systemd-boot-system-token.service8 diff --git a/man/loader.conf.xml b/man/loader.conf.xml index 38a80861b8..14f84c13ee 100644 --- a/man/loader.conf.xml +++ b/man/loader.conf.xml @@ -153,6 +153,25 @@ Takes a boolean argument. Enable (the default) or disable the "Reboot into firmware" entry. + + + random-seed-mode + + Takes one of off, with-system-token and + always. If off no random seed data is read off the ESP, nor + passed to the OS. If with-system-token (the default) + systemd-boot will read a random seed from the ESP (from the file + /loader/random-seed) only if the LoaderSystemToken EFI + variable is set, and then derive the random seed to pass to the OS from the combination. If + always the boot loader will do so even if LoaderSystemToken is + not set. This mode is useful in environments where protection against OS image reuse is not a + concern, and the random seed shall be used even with no further setup in place. User bootctl + random-seed to initialize both the random seed file in the ESP and the system token EFI + variable. + + See Random Seeds for further + information. + diff --git a/man/rules/meson.build b/man/rules/meson.build index 7e32e732c1..3b63311d7b 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -655,6 +655,7 @@ manpages = [ ['systemd-bless-boot-generator', '8', [], 'ENABLE_EFI'], ['systemd-bless-boot.service', '8', [], 'ENABLE_EFI'], ['systemd-boot-check-no-failures.service', '8', [], ''], + ['systemd-boot-system-token.service', '8', [], 'ENABLE_EFI'], ['systemd-boot', '7', ['sd-boot'], 'ENABLE_EFI'], ['systemd-cat', '1', [], ''], ['systemd-cgls', '1', [], ''], diff --git a/man/systemd-boot-system-token.service.xml b/man/systemd-boot-system-token.service.xml new file mode 100644 index 0000000000..b2948a5c4b --- /dev/null +++ b/man/systemd-boot-system-token.service.xml @@ -0,0 +1,76 @@ + + + + + + + + systemd-boot-system-token.service + systemd + + + + systemd-boot-system-token.service + 8 + + + + systemd-boot-system-token.service + Generate an initial boot loader system token and random seed + + + + systemd-boot-system-token.service + + + + Description + + systemd-boot-system-token.service is a system service that automatically + generates a 'system token' to store in an EFI variable in the system's NVRAM and a random seed to store + on the EFI System Partition ESP on disk. The boot loader may then combine these two randomized data + fields by cryptographic hashing, and pass it to the OS it boots as initialization seed for its entropy + pool. The random seed stored in the ESP is refreshed on each reboot ensuring that multiple subsequent + boots will boot with different seeds. The 'system token' is generated randomly once, and then + persistently stored in the system's EFI variable storage. + + The systemd-boot-system-token.service unit invokes the bootctl + random-seed command, which updates the random seed in the ESP, and initializes the 'system + token' if it's not initialized yet. The service is conditionalized so that it is run only when all of the + below apply: + + + A boot loader is used that implements the Boot Loader Interface (which defines the 'system + token' concept). + + Either a 'system token' was not set yet, or the boot loader has not passed the OS a + random seed yet (and thus most likely has been missing the random seed file in the + ESP). + + The system is not running in a VM environment. This case is explicitly excluded since + on VM environments the ESP backing storage and EFI variable storage is typically not physically + separated and hence booting the same OS image in multiple instances would replicate both, thus reusing + the same random seed and 'system token' among all instances, which defeats its purpose. Note that it's + still possible to use boot loader random seed provisioning in this mode, but the automatic logic + implemented by this service has no effect then, and the user instead has to manually invoke the + bootctl random-seed acknowledging these restrictions. + + + For further details see + bootctl1, regarding + the command this service invokes. + + + + See Also + + systemd1, + bootctl1, + systemd-boot7 + + + + diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml index 2575ab3fe5..da8ddb5f84 100644 --- a/man/systemd-boot.xml +++ b/man/systemd-boot.xml @@ -28,13 +28,14 @@ manager. It provides a graphical menu to select the entry to boot and an editor for the kernel command line. systemd-boot supports systems with UEFI firmware only. - systemd-boot loads boot entry information from the EFI system partition (ESP), usually mounted at - /efi/, /boot/, or /boot/efi/ during OS - runtime, as well as from the Extended Boot Loader partition if it exists (usually mounted to - /boot/). Configuration file fragments, kernels, initrds and other EFI images to boot - generally need to reside on the ESP or the Extended Boot Loader partition. Linux kernels must be built - with to be able to be directly executed as an EFI image. During boot - systemd-boot automatically assembles a list of boot entries from the following sources: + systemd-boot loads boot entry information from the EFI system partition (ESP), + usually mounted at /efi/, /boot/, or + /boot/efi/ during OS runtime, as well as from the Extended Boot Loader partition if + it exists (usually mounted to /boot/). Configuration file fragments, kernels, + initrds and other EFI images to boot generally need to reside on the ESP or the Extended Boot Loader + partition. Linux kernels must be built with to be able to be directly + executed as an EFI image. During boot systemd-boot automatically assembles a list of + boot entries from the following sources: Boot entries defined with A reboot into the UEFI firmware setup option, if supported by the firmware - kernel-install8 - may be used to copy kernel images onto the ESP or the Extended Boot Loader Partition and to generate - description files compliant with the Boot Loader - Specification. bootctl1 + systemd-boot supports the following features: + + + Basic boot manager configuration changes (such as timeout + configuration, default boot entry selection, …) may be made directly from the boot loader UI at + boot-time, as well as during system runtime with EFI variables. + + The boot manager integrates with the systemctl command to implement + features such as systemctl reboot --boot-loader-entry=… (for rebooting into a + specific boot menu entry, i.e. "reboot into Windows") and systemctl reboot + --boot-loader-menu=… (for rebooting into the boot loader menu), by implementing the Boot Loader Interface. See + systemctl1 for + details. + + An EFI variable set by the boot loader informs the OS about the ESP partition used + during boot. This is then used to automatically mount the correct ESP partition to + /efi/ or /boot/ during OS runtime. See + systemd-gpt-auto-generator8 + for details. + + The boot manager provides information about the boot time spent in UEFI firmware using + the Boot Loader Interface. This + information can be displayed using + systemd-analyze1. + + + The boot manager implements boot counting and automatic fallback to older, working boot + entries on failure. See Automatic Boot + Assessment. + + The boot manager optionally reads a random seed from the ESP partition, combines it + with a 'system token' stored in a persistant EFI variable and derives a random seed to use by the OS as + entropy pool initializaton, providing a full entropy pool during early boot. + + + bootctl1 may be used from a running system to locate the ESP and the Extended Boot Loader Partition, list available entries, and install systemd-boot itself. - systemd-boot will provide information about the time spent in UEFI firmware using the Boot Loader Interface. This information can be displayed - using systemd-analyze1. - + kernel-install8 + may be used to copy kernel images onto the ESP or the Extended Boot Loader Partition and to generate + description files compliant with the Boot Loader + Specification. @@ -238,7 +272,9 @@ Loader Specification are read from /loader/entries/ on the ESP and the Extended Boot Loader partition. Unified kernel boot entries following the Boot Loader Specification are read from - /EFI/Linux/ on the ESP and the Extended Boot Loader partition. + /EFI/Linux/ on the ESP and the Extended Boot Loader partition. Optionally, a random + seed for early boot entropy pool provisioning is stored in /loader/random-seed in + the ESP. @@ -346,10 +382,42 @@ Information about the time spent in various parts of the boot loader. Set by the boot loader. Use systemd-analyze1 - to view this data. These variables are defined by the Boot Loader Interface. + to view this data. + + + + LoaderRandomSeed + + A binary random seed systemd-boot may optionally pass to the + OS. This is a volatile EFI variable that is hashed at boot from the combination of a random seed + stored in the ESP (in /loader/random-seed) and a "system token" persistently + stored in the EFI variable LoaderSystemToken (see below). During early OS boot the + system manager reads this variable and passes it to the OS kernel's random pool, crediting the full + entropy it contains. This is an efficient way to ensure the system starts up with a fully initialized + kernel random pool — as early as the initial RAM disk phase. systemd-boot reads + the random seed from the ESP, combines it with the "system token", and both derives a new random seed + to update in-place the seed stored in the ESP, and the random seed to pass to the OS from it via + SHA256 hashing in counter mode. This ensures that different physical systems that boot the same + "golden" OS image — i.e. containing the same random seed file in the ESP — will still pass a + different random seed to the OS. It is made sure the random seed stored in the ESP is fully + overwritten before the OS is booted, to ensure different random seed data is used between subsequent + boots. + + See Random Seeds for + further information. + + + + LoaderSystemToken + + A binary random data field, that is used for generating the random see to pass to the + OS (see above). Note that this random data is generally only generated once, during OS installation, + and is then never updated again. + + Many of these variables are defined by the Boot Loader Interface. @@ -413,6 +481,7 @@ bootctl1, loader.conf5, systemd-bless-boot.service8, + systemd-boot-system-token.service8, kernel-install8, Boot Loader Specification, Boot Loader Interface diff --git a/man/systemd-random-seed.service.xml b/man/systemd-random-seed.service.xml index 35c6e2fd0b..28783a15e9 100644 --- a/man/systemd-random-seed.service.xml +++ b/man/systemd-random-seed.service.xml @@ -29,21 +29,63 @@ Description - systemd-random-seed.service is a - service that restores the random seed of the system at early boot - and saves it at shutdown. See - random4 - for details. Saving/restoring the random seed across boots - increases the amount of available entropy early at boot. On disk - the random seed is stored in - /var/lib/systemd/random-seed. + systemd-random-seed.service is a service that loads an on-disk random seed + into the kernel entropy pool during boot and saves it at shutdown. See + random4 for + details. By default, no entropy is credited when the random seed is written into the kernel entropy pool, + but this may be changed with $SYSTEMD_RANDOM_SEED_CREDIT, see below. On disk the random + seed is stored in /var/lib/systemd/random-seed. + + Note that this service runs relatively late during the early boot phase, i.e. generally after the + initial RAM disk (initrd) completed its work, and the /var/ file system has been + mounted writable. Many system services require entropy much earlier than this — this service is hence of + limited use for complex system. It is recommended to use a boot loader that can pass an initial random + seed to the kernel to ensure that entropy is available from earliest boot on, for example + systemd-boot7, with + its bootctl random-seed functionality. + + When loading the random seed from disk its file is immediately updated with a new seed retrieved + from the kernel, in order to ensure no two boots operate with the same random seed. This new seed is + retrieved synchronously from the kernel, which means the service will not complete start-up until the + random pool is fully initialized. On entropy-starved systems this may take a while. This functionality is + intended to be used as synchronization point for ordering services that require an initialized entropy + pool to function securely (i.e. services that access /dev/urandom without any + further precautions). + + Care should be taken when creating OS images that are replicated to multiple systems: if the random + seed file is included unmodified each system will initialize its entropy pool with the same data, and + thus — if otherwise entropy-starved — generate the same or at least guessable random seed streams. As a + safety precaution crediting entropy is thus disabled by default. It is recommended to remove the random + seed from OS images intended for replication on multiple systems, in which case it is safe to enable + entropy crediting, see below. + + See Random Seeds for further + information. + + + + Environment + + + + $SYSTEMD_RANDOM_SEED_CREDIT + By default, systemd-random-seed.service does not credit any + entropy when loading the random seed. With this option this behaviour may be changed: it either takes + a boolean parameter or the special string force. Defaults to false, in which case + no entropy is credited. If true, entropy is credited if the random seed file and system state pass + various superficial concisistency checks. If set to force entropy is credited, + regardless of these checks, as long as the random seed file exists. + + See Also systemd1, - random4 + random4, + systemd-boot7, + bootctl4 diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 3ed8e2c8a9..f929db9c23 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -1298,6 +1298,17 @@ int fsync_directory_of_file(int fd) { return 0; } +int fsync_full(int fd) { + int r, q; + + /* Sync both the file and the directory */ + + r = fsync(fd) < 0 ? -errno : 0; + q = fsync_directory_of_file(fd); + + return r < 0 ? r : q; +} + int fsync_path_at(int at_fd, const char *path) { _cleanup_close_ int opened_fd = -1; int fd; diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index c5527cc44f..c7c5899815 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -114,6 +114,7 @@ void unlink_tempfilep(char (*p)[]); int unlinkat_deallocate(int fd, const char *name, int flags); int fsync_directory_of_file(int fd); +int fsync_full(int fd); int fsync_path_at(int at_fd, const char *path); int syncfs_path(int atfd, const char *path); diff --git a/src/basic/random-util.c b/src/basic/random-util.c index 3af6f271f0..b6a9ad4060 100644 --- a/src/basic/random-util.c +++ b/src/basic/random-util.c @@ -25,8 +25,10 @@ #include "alloc-util.h" #include "fd-util.h" +#include "fileio.h" #include "io-util.h" #include "missing.h" +#include "parse-util.h" #include "random-util.h" #include "siphash24.h" #include "time-util.h" @@ -389,3 +391,26 @@ void random_bytes(void *p, size_t n) { /* If for some reason some user made /dev/urandom unavailable to us, or the kernel has no entropy, use a PRNG instead. */ pseudo_random_bytes(p, n); } + +size_t random_pool_size(void) { + _cleanup_free_ char *s = NULL; + int r; + + /* Read pool size, if possible */ + r = read_one_line_file("/proc/sys/kernel/random/poolsize", &s); + if (r < 0) + log_debug_errno(r, "Failed to read pool size from kernel: %m"); + else { + unsigned sz; + + r = safe_atou(s, &sz); + if (r < 0) + log_debug_errno(r, "Failed to parse pool size: %s", s); + else + /* poolsize is in bits on 2.6, but we want bytes */ + return CLAMP(sz / 8, RANDOM_POOL_SIZE_MIN, RANDOM_POOL_SIZE_MAX); + } + + /* Use the minimum as default, if we can't retrieve the correct value */ + return RANDOM_POOL_SIZE_MIN; +} diff --git a/src/basic/random-util.h b/src/basic/random-util.h index 148b6c7813..facc11b976 100644 --- a/src/basic/random-util.h +++ b/src/basic/random-util.h @@ -31,3 +31,9 @@ static inline uint32_t random_u32(void) { } int rdrand(unsigned long *ret); + +/* Some limits on the pool sizes when we deal with the kernel random pool */ +#define RANDOM_POOL_SIZE_MIN 512U +#define RANDOM_POOL_SIZE_MAX (10U*1024U*1024U) + +size_t random_pool_size(void); diff --git a/src/basic/xattr-util.c b/src/basic/xattr-util.c index 1d045f911b..ab321e0293 100644 --- a/src/basic/xattr-util.c +++ b/src/basic/xattr-util.c @@ -27,7 +27,7 @@ int getxattr_malloc(const char *path, const char *name, char **value, bool allow assert(name); assert(value); - for (l = 100; ; l = (size_t) n + 1) { + for (l = 100; ; l = (size_t) n + 1 /* extra byte to make sure this remains NUL suffixed */) { v = new0(char, l); if (!v) return -ENOMEM; @@ -36,7 +36,6 @@ int getxattr_malloc(const char *path, const char *name, char **value, bool allow n = lgetxattr(path, name, v, l); else n = getxattr(path, name, v, l); - if (n >= 0 && (size_t) n < l) { *value = v; return n; @@ -65,13 +64,12 @@ int fgetxattr_malloc(int fd, const char *name, char **value) { assert(name); assert(value); - for (l = 100; ; l = (size_t) n + 1) { + for (l = 100;; l = (size_t) n + 1 /* extra byte to make sure this remains NUL suffixed */) { v = new0(char, l); if (!v) return -ENOMEM; n = fgetxattr(fd, name, v, l); - if (n >= 0 && (size_t) n < l) { *value = v; return n; diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index d570526a4f..e7cf73e1a1 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -25,6 +25,7 @@ #include "copy.h" #include "dirent-util.h" #include "efivars.h" +#include "env-util.h" #include "escape.h" #include "fd-util.h" #include "fileio.h" @@ -34,6 +35,7 @@ #include "pager.h" #include "parse-util.h" #include "pretty-print.h" +#include "random-util.h" #include "rm-rf.h" #include "stat-util.h" #include "stdio-util.h" @@ -580,21 +582,29 @@ static int mkdir_one(const char *prefix, const char *suffix) { } static const char *const esp_subdirs[] = { + /* The directories to place in the ESP */ "EFI", "EFI/systemd", "EFI/BOOT", "loader", - /* Note that "/loader/entries" is not listed here, since it should be placed in $BOOT, which might - * not necessarily be the ESP */ NULL }; -static int create_esp_subdirs(const char *esp_path) { +static const char *const dollar_boot_subdirs[] = { + /* The directories to place in the XBOOTLDR partition or the ESP, depending what exists */ + "loader", + "loader/entries", /* Type #1 entries */ + "EFI", + "EFI/Linux", /* Type #2 entries */ + NULL +}; + +static int create_subdirs(const char *root, const char * const *subdirs) { const char *const *i; int r; - STRV_FOREACH(i, esp_subdirs) { - r = mkdir_one(esp_path, *i); + STRV_FOREACH(i, subdirs) { + r = mkdir_one(root, *i); if (r < 0) return r; } @@ -603,6 +613,7 @@ static int create_esp_subdirs(const char *esp_path) { } static int copy_one_file(const char *esp_path, const char *name, bool force) { + const char *e; char *p, *q; int r; @@ -610,13 +621,13 @@ static int copy_one_file(const char *esp_path, const char *name, bool force) { q = strjoina(esp_path, "/EFI/systemd/", name); r = copy_file_with_version_check(p, q, force); - if (startswith(name, "systemd-boot")) { + e = startswith(name, "systemd-boot"); + if (e) { int k; char *v; /* Create the EFI default boot loader name (specified for removable devices) */ - v = strjoina(esp_path, "/EFI/BOOT/BOOT", - name + STRLEN("systemd-boot")); + v = strjoina(esp_path, "/EFI/BOOT/BOOT", e); ascii_strupper(strrchr(v, '/') + 1); k = copy_file_with_version_check(p, v, force); @@ -650,7 +661,7 @@ static int install_binaries(const char *esp_path, bool force) { return r; } -static bool same_entry(uint16_t id, const sd_id128_t uuid, const char *path) { +static bool same_entry(uint16_t id, sd_id128_t uuid, const char *path) { _cleanup_free_ char *opath = NULL; sd_id128_t ouuid; int r; @@ -864,19 +875,27 @@ static int rmdir_one(const char *prefix, const char *suffix) { return 0; } -static int remove_esp_subdirs(const char *esp_path) { - size_t i; - int r = 0; +static int remove_subdirs(const char *root, const char *const *subdirs) { + int r, q; - for (i = ELEMENTSOF(esp_subdirs)-1; i > 0; i--) { - int q; + /* We use recursion here to destroy the directories in reverse order. Which should be safe given how + * short the array is. */ - q = rmdir_one(esp_path, esp_subdirs[i-1]); - if (q < 0 && r >= 0) - r = q; - } + if (!subdirs[0]) /* A the end of the list */ + return 0; - return r; + r = remove_subdirs(root, subdirs + 1); + q = rmdir_one(root, subdirs[0]); + + return r < 0 ? r : q; +} + +static int remove_machine_id_directory(const char *root, sd_id128_t machine_id) { + char buf[SD_ID128_STRING_MAX]; + + assert(root); + + return rmdir_one(root, sd_id128_to_string(machine_id, buf)); } static int remove_binaries(const char *esp_path) { @@ -893,26 +912,22 @@ static int remove_binaries(const char *esp_path) { return r; } -static int remove_loader_config(const char *esp_path) { +static int remove_file(const char *root, const char *file) { const char *p; - assert(esp_path); + assert(root); + assert(file); - p = prefix_roota(esp_path, "/loader/loader.conf"); + p = prefix_roota(root, file); if (unlink(p) < 0) { - log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, "Failed to unlink file \"%s\": %m", p); - if (errno != ENOENT) - return -errno; - } else - log_info("Removed \"%s\".", p); + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to unlink file \"%s\": %m", p); - return 0; -} + return errno == ENOENT ? 0 : -errno; + } -static int remove_entries_directory(const char *dollar_boot_path) { - assert(dollar_boot_path); - - return rmdir_one(dollar_boot_path, "/loader/entries"); + log_info("Removed \"%s\".", p); + return 1; } static int remove_variables(sd_id128_t uuid, const char *path, bool in_order) { @@ -936,6 +951,35 @@ static int remove_variables(sd_id128_t uuid, const char *path, bool in_order) { return 0; } +static int remove_loader_variables(void) { + const char *p; + int r = 0; + + /* Remove all persistent loader variables we define */ + + FOREACH_STRING(p, + "LoaderConfigTimeout", + "LoaderConfigTimeoutOneShot", + "LoaderEntryDefault", + "LoaderEntryOneShot", + "LoaderSystemToken") { + + int q; + + q = efi_set_variable(EFI_VENDOR_LOADER, p, NULL, 0); + if (q == -ENOENT) + continue; + if (q < 0) { + log_warning_errno(q, "Failed to remove %s variable: %m", p); + if (r >= 0) + r = q; + } else + log_info("Removed EFI variable %s.", p); + } + + return r; +} + static int install_loader_config(const char *esp_path, sd_id128_t machine_id) { char machine_string[SD_ID128_STRING_MAX]; _cleanup_(unlink_and_freep) char *t = NULL; @@ -975,21 +1019,12 @@ static int install_loader_config(const char *esp_path, sd_id128_t machine_id) { return 1; } -static int install_entries_directories(const char *dollar_boot_path, sd_id128_t machine_id) { - int r; +static int install_machine_id_directory(const char *root, sd_id128_t machine_id) { char buf[SD_ID128_STRING_MAX]; - assert(dollar_boot_path); + assert(root); - /* Both /loader/entries and the entry directories themselves should be located on the same - * partition. Also create the parent directory for entry directories, so that kernel-install - * knows where to put them. */ - - r = mkdir_one(dollar_boot_path, "loader/entries"); - if (r < 0) - return r; - - return mkdir_one(dollar_boot_path, sd_id128_to_string(machine_id, buf)); + return mkdir_one(root, sd_id128_to_string(machine_id, buf)); } static int help(int argc, char *argv[], void *userdata) { @@ -1015,6 +1050,8 @@ static int help(int argc, char *argv[], void *userdata) { " install Install systemd-boot to the ESP and EFI variables\n" " update Update systemd-boot in the ESP and EFI variables\n" " remove Remove systemd-boot from the ESP and EFI variables\n" + " random-seed Initialize random seed in ESP and EFI variables\n" + " is-installed Test whether systemd-boot is installed in the ESP\n" "\nBoot Loader Entries Commands:\n" " list List boot loader entries\n" " set-default ID Set default boot loader entry\n" @@ -1154,12 +1191,13 @@ static int verb_status(int argc, char *argv[], void *userdata) { uint64_t flag; const char *name; } flags[] = { - { EFI_LOADER_FEATURE_BOOT_COUNTING, "Boot counting" }, - { EFI_LOADER_FEATURE_CONFIG_TIMEOUT, "Menu timeout control" }, - { EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT, "One-shot menu timeout control" }, - { EFI_LOADER_FEATURE_ENTRY_DEFAULT, "Default entry control" }, - { EFI_LOADER_FEATURE_ENTRY_ONESHOT, "One-shot entry control" }, - { EFI_LOADER_FEATURE_XBOOTLDR, "Support for XBOOTLDR partition" }, + { EFI_LOADER_FEATURE_BOOT_COUNTING, "Boot counting" }, + { EFI_LOADER_FEATURE_CONFIG_TIMEOUT, "Menu timeout control" }, + { EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT, "One-shot menu timeout control" }, + { EFI_LOADER_FEATURE_ENTRY_DEFAULT, "Default entry control" }, + { EFI_LOADER_FEATURE_ENTRY_ONESHOT, "One-shot entry control" }, + { EFI_LOADER_FEATURE_XBOOTLDR, "Support for XBOOTLDR partition" }, + { EFI_LOADER_FEATURE_RANDOM_SEED, "Support for passing random seed to OS" }, }; _cleanup_free_ char *fw_type = NULL, *fw_info = NULL, *loader = NULL, *loader_path = NULL, *stub = NULL; @@ -1212,6 +1250,22 @@ static int verb_status(int argc, char *argv[], void *userdata) { printf(" ESP: n/a\n"); printf(" File: %s%s\n", special_glyph(SPECIAL_GLYPH_TREE_RIGHT), strna(loader_path)); printf("\n"); + + printf("Random Seed:\n"); + printf(" Passed to OS: %s\n", yes_no(access("/sys/firmware/efi/efivars/LoaderRandomSeed-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", F_OK) >= 0)); + printf(" System Token: %s\n", access("/sys/firmware/efi/efivars/LoaderSystemToken-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", F_OK) >= 0 ? "set" : "not set"); + + if (arg_esp_path) { + _cleanup_free_ char *p = NULL; + + p = path_join(arg_esp_path, "/loader/random-seed"); + if (!p) + return log_oom(); + + printf(" Exists: %s\n", yes_no(access(p, F_OK) >= 0)); + } + + printf("\n"); } else printf("System:\n Not booted with EFI\n\n"); @@ -1284,6 +1338,122 @@ static int verb_list(int argc, char *argv[], void *userdata) { return 0; } +static int install_random_seed(const char *esp) { + _cleanup_(unlink_and_freep) char *tmp = NULL; + _cleanup_free_ void *buffer = NULL; + _cleanup_free_ char *path = NULL; + _cleanup_close_ int fd = -1; + size_t sz, token_size; + ssize_t n; + int r; + + assert(esp); + + path = path_join(esp, "/loader/random-seed"); + if (!path) + return log_oom(); + + sz = random_pool_size(); + + buffer = malloc(sz); + if (!buffer) + return log_oom(); + + r = genuine_random_bytes(buffer, sz, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Faile to acquire random seed: %m"); + + r = tempfn_random(path, "bootctl", &tmp); + if (r < 0) + return log_oom(); + + fd = open(tmp, O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY|O_WRONLY|O_CLOEXEC, 0600); + if (fd < 0) { + tmp = mfree(tmp); + return log_error_errno(fd, "Failed to open random seed file for writing: %m"); + } + + n = write(fd, buffer, sz); + if (n < 0) + return log_error_errno(errno, "Failed to write random seed file: %m"); + if ((size_t) n != sz) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while writing random seed file."); + + if (rename(tmp, path) < 0) + return log_error_errno(r, "Failed to move random seed file into place: %m"); + + tmp = mfree(tmp); + + log_info("Successfully written random seed file %s with %zu bytes.", path, sz); + + if (!arg_touch_variables) + return 0; + + if (!is_efi_boot()) { + log_notice("Not booted with EFI, skipping EFI variable setup."); + return 0; + } + + r = getenv_bool("SYSTEMD_WRITE_SYSTEM_TOKEN"); + if (r < 0) { + if (r != -ENXIO) + log_warning_errno(r, "Failed to parse $SYSTEMD_WRITE_SYSTEM_TOKEN, ignoring."); + + if (detect_vm() > 0) { + /* Let's not write a system token if we detect we are running in a VM + * environment. Why? Our default security model for the random seed uses the system + * token as a mechanism to ensure we are not vulnerable to golden master sloppiness + * issues, i.e. that people initialize the random seed file, then copy the image to + * many systems and end up with the same random seed in each that is assumed to be + * valid but in reality is the same for all machines. By storing a system token in + * the EFI variable space we can make sure that even though the random seeds on disk + * are all the same they will be different on each system under the assumption that + * the EFI variable space is maintained separate from the random seed storage. That + * is generally the case on physical systems, as the ESP is stored on persistant + * storage, and the EFI variables in NVRAM. However in virtualized environments this + * is generally not true: the EFI variable set is typically stored along with the + * disk image itself. For example, using the OVMF EFI firmware the EFI variables are + * stored in a file in the ESP itself. */ + + log_notice("Not installing system token, since we are running in a virtualized environment."); + return 0; + } + } else if (r == 0) { + log_notice("Not writing system token, because $SYSTEMD_WRITE_SYSTEM_TOKEN is set to false."); + return 0; + } + + r = efi_get_variable(EFI_VENDOR_LOADER, "LoaderSystemToken", NULL, NULL, &token_size); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to test system token validity: %m"); + } else { + if (token_size >= sz) { + /* Let's avoid writes if we can, and initialize this only once. */ + log_debug("System token already written, not updating."); + return 0; + } + + log_debug("Existing system token size (%zu) does not match our expectations (%zu), replacing.", token_size, sz); + } + + r = genuine_random_bytes(buffer, sz, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to acquire random seed: %m"); + + /* Let's write this variable with an umask in effect, so that unprivileged users can't see the token + * and possibly get identification information or too much insight into the kernel's entropy pool + * state. */ + RUN_WITH_UMASK(0077) { + r = efi_set_variable(EFI_VENDOR_LOADER, "LoaderSystemToken", buffer, sz); + if (r < 0) + return log_error_errno(r, "Failed to set LoaderSystemToken EFI variable: %m"); + } + + log_info("Successfully initialized system token in EFI variable with %zu bytes.", sz); + return 0; +} + static int sync_everything(void) { int ret = 0, k; @@ -1329,7 +1499,11 @@ static int verb_install(int argc, char *argv[], void *userdata) { /* Don't create any of these directories when we are just updating. When we update * we'll drop-in our files (unless there are newer ones already), but we won't create * the directories for them in the first place. */ - r = create_esp_subdirs(arg_esp_path); + r = create_subdirs(arg_esp_path, esp_subdirs); + if (r < 0) + return r; + + r = create_subdirs(arg_dollar_boot_path(), dollar_boot_subdirs); if (r < 0) return r; } @@ -1343,7 +1517,11 @@ static int verb_install(int argc, char *argv[], void *userdata) { if (r < 0) return r; - r = install_entries_directories(arg_dollar_boot_path(), machine_id); + r = install_machine_id_directory(arg_dollar_boot_path(), machine_id); + if (r < 0) + return r; + + r = install_random_seed(arg_esp_path); if (r < 0) return r; } @@ -1361,7 +1539,7 @@ static int verb_install(int argc, char *argv[], void *userdata) { } static int verb_remove(int argc, char *argv[], void *userdata) { - sd_id128_t uuid = SD_ID128_NULL; + sd_id128_t uuid = SD_ID128_NULL, machine_id; int r, q; r = acquire_esp(false, NULL, NULL, NULL, &uuid); @@ -1372,31 +1550,97 @@ static int verb_remove(int argc, char *argv[], void *userdata) { if (r < 0) return r; + r = sd_id128_get_machine(&machine_id); + if (r < 0) + return log_error_errno(r, "Failed to get machine id: %m"); + r = remove_binaries(arg_esp_path); - q = remove_loader_config(arg_esp_path); + q = remove_file(arg_esp_path, "/loader/loader.conf"); if (q < 0 && r >= 0) r = q; - q = remove_entries_directory(arg_dollar_boot_path()); + q = remove_file(arg_esp_path, "/loader/random-seed"); if (q < 0 && r >= 0) r = q; - q = remove_esp_subdirs(arg_esp_path); + q = remove_subdirs(arg_esp_path, esp_subdirs); if (q < 0 && r >= 0) r = q; - (void) sync_everything(); + q = remove_subdirs(arg_esp_path, dollar_boot_subdirs); + if (q < 0 && r >= 0) + r = q; - if (arg_touch_variables) { - q = remove_variables(uuid, "/EFI/systemd/systemd-boot" EFI_MACHINE_TYPE_NAME ".efi", true); + q = remove_machine_id_directory(arg_esp_path, machine_id); + if (q < 0 && r >= 0) + r = 1; + + if (arg_xbootldr_path) { + /* Remove the latter two also in the XBOOTLDR partition if it exists */ + q = remove_subdirs(arg_xbootldr_path, dollar_boot_subdirs); + if (q < 0 && r >= 0) + r = q; + + q = remove_machine_id_directory(arg_xbootldr_path, machine_id); if (q < 0 && r >= 0) r = q; } + (void) sync_everything(); + + if (!arg_touch_variables) + return r; + + q = remove_variables(uuid, "/EFI/systemd/systemd-boot" EFI_MACHINE_TYPE_NAME ".efi", true); + if (q < 0 && r >= 0) + r = q; + + q = remove_loader_variables(); + if (q < 0 && r >= 0) + r = q; + return r; } +static int verb_is_installed(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *p = NULL; + int r; + + r = acquire_esp(false, NULL, NULL, NULL, NULL); + if (r < 0) + return r; + + /* Tests whether systemd-boot is installed. It's not obvious what to use as check here: we could + * check EFI variables, we could check what binary /EFI/BOOT/BOOT*.EFI points to, or whether the + * loader entries directory exists. Here we opted to check whether /EFI/systemd/ is non-empty, which + * should be a suitable and very minimal check for a number of reasons: + * + * → The check is architecture independent (i.e. we check if any systemd-boot loader is installed, not a + * specific one.) + * + * → It doesn't assume we are the only boot loader (i.e doesn't check if we own the main + * /EFI/BOOT/BOOT*.EFI fallback binary. + * + * → It specifically checks for systemd-boot, not for other boot loaders (which a check for + * /boot/loader/entries would do). */ + + p = path_join(arg_esp_path, "/EFI/systemd/"); + if (!p) + return log_oom(); + + r = dir_is_empty(p); + if (r > 0 || r == -ENOENT) { + puts("no"); + return EXIT_FAILURE; + } + if (r < 0) + return log_error_errno(r, "Failed to detect whether systemd-boot is installed: %m"); + + puts("yes"); + return EXIT_SUCCESS; +} + static int verb_set_default(int argc, char *argv[], void *userdata) { const char *name; int r; @@ -1445,16 +1689,33 @@ static int verb_set_default(int argc, char *argv[], void *userdata) { return 0; } +static int verb_random_seed(int argc, char *argv[], void *userdata) { + int r; + + r = acquire_esp(false, NULL, NULL, NULL, NULL); + if (r < 0) + return r; + + r = install_random_seed(arg_esp_path); + if (r < 0) + return r; + + (void) sync_everything(); + return 0; +} + static int bootctl_main(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, help }, - { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, - { "install", VERB_ANY, 1, 0, verb_install }, - { "update", VERB_ANY, 1, 0, verb_install }, - { "remove", VERB_ANY, 1, 0, verb_remove }, - { "list", VERB_ANY, 1, 0, verb_list }, - { "set-default", 2, 2, 0, verb_set_default }, - { "set-oneshot", 2, 2, 0, verb_set_default }, + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, + { "install", VERB_ANY, 1, 0, verb_install }, + { "update", VERB_ANY, 1, 0, verb_install }, + { "remove", VERB_ANY, 1, 0, verb_remove }, + { "random-seed", VERB_ANY, 1, 0, verb_random_seed }, + { "is-installed", VERB_ANY, 1, 0, verb_is_installed }, + { "list", VERB_ANY, 1, 0, verb_list }, + { "set-default", 2, 2, 0, verb_set_default }, + { "set-oneshot", 2, 2, 0, verb_set_default }, {} }; diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index e0f75562c0..fd0c972951 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -12,6 +12,7 @@ #include "loader-features.h" #include "measure.h" #include "pe.h" +#include "random-seed.h" #include "shim.h" #include "util.h" @@ -68,6 +69,7 @@ typedef struct { BOOLEAN force_menu; UINTN console_mode; enum console_mode_change_type console_mode_change; + RandomSeedMode random_seed_mode; } Config; static VOID cursor_left(UINTN *cursor, UINTN *first) { @@ -396,6 +398,21 @@ static VOID print_status(Config *config, CHAR16 *loaded_image_path) { Print(L"editor: %s\n", yes_no(config->editor)); Print(L"auto-entries: %s\n", yes_no(config->auto_entries)); Print(L"auto-firmware: %s\n", yes_no(config->auto_firmware)); + + switch (config->random_seed_mode) { + case RANDOM_SEED_OFF: + Print(L"random-seed-mode: off\n"); + break; + case RANDOM_SEED_WITH_SYSTEM_TOKEN: + Print(L"random-seed-node: with-system-token\n"); + break; + case RANDOM_SEED_ALWAYS: + Print(L"random-seed-node: always\n"); + break; + default: + ; + } + Print(L"\n"); Print(L"config entry count: %d\n", config->entry_count); @@ -1038,7 +1055,9 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { if (EFI_ERROR(parse_boolean(value, &on))) continue; + config->editor = on; + continue; } if (strcmpa((CHAR8 *)"auto-entries", key) == 0) { @@ -1046,7 +1065,9 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { if (EFI_ERROR(parse_boolean(value, &on))) continue; + config->auto_entries = on; + continue; } if (strcmpa((CHAR8 *)"auto-firmware", key) == 0) { @@ -1054,7 +1075,9 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { if (EFI_ERROR(parse_boolean(value, &on))) continue; + config->auto_firmware = on; + continue; } if (strcmpa((CHAR8 *)"console-mode", key) == 0) { @@ -1074,6 +1097,23 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { continue; } + + if (strcmpa((CHAR8*) "random-seed-mode", key) == 0) { + if (strcmpa((CHAR8*) "off", value) == 0) + config->random_seed_mode = RANDOM_SEED_OFF; + else if (strcmpa((CHAR8*) "with-system-token", value) == 0) + config->random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN; + else if (strcmpa((CHAR8*) "always", value) == 0) + config->random_seed_mode = RANDOM_SEED_ALWAYS; + else { + BOOLEAN on; + + if (EFI_ERROR(parse_boolean(value, &on))) + continue; + + config->random_seed_mode = on ? RANDOM_SEED_ALWAYS : RANDOM_SEED_OFF; + } + } } } @@ -1404,6 +1444,7 @@ static VOID config_load_defaults(Config *config, EFI_FILE *root_dir) { .editor = TRUE, .auto_entries = TRUE, .auto_firmware = TRUE, + .random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN, }; err = file_read(root_dir, L"\\loader\\loader.conf", 0, 0, &content, NULL); @@ -2284,6 +2325,7 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { EFI_LOADER_FEATURE_ENTRY_ONESHOT | EFI_LOADER_FEATURE_BOOT_COUNTING | EFI_LOADER_FEATURE_XBOOTLDR | + EFI_LOADER_FEATURE_RANDOM_SEED | 0; _cleanup_freepool_ CHAR16 *infostr = NULL, *typestr = NULL; @@ -2435,8 +2477,11 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { config_entry_bump_counters(entry, root_dir); - /* export the selected boot entry to the system */ - efivar_set(L"LoaderEntrySelected", entry->id, FALSE); + /* Export the selected boot entry to the system */ + (VOID) efivar_set(L"LoaderEntrySelected", entry->id, FALSE); + + /* Optionally, read a random seed off the ESP and pass it to the OS */ + (VOID) process_random_seed(root_dir, config.random_seed_mode); uefi_call_wrapper(BS->SetWatchdogTimer, 4, 5 * 60, 0x10000, 0, NULL); err = image_start(image, &config, entry); diff --git a/src/boot/efi/loader-features.h b/src/boot/efi/loader-features.h index 40095e28ec..b3a1f79dfd 100644 --- a/src/boot/efi/loader-features.h +++ b/src/boot/efi/loader-features.h @@ -11,3 +11,4 @@ #define EFI_LOADER_FEATURE_ENTRY_ONESHOT (UINT64_C(1) << 3) #define EFI_LOADER_FEATURE_BOOT_COUNTING (UINT64_C(1) << 4) #define EFI_LOADER_FEATURE_XBOOTLDR (UINT64_C(1) << 5) +#define EFI_LOADER_FEATURE_RANDOM_SEED (UINT64_C(1) << 6) diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index 0ae3191635..277432c7e0 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -8,6 +8,8 @@ efi_headers = files(''' linux.h measure.h pe.h + random-seed.h + sha256.h shim.h splash.h util.h @@ -24,8 +26,10 @@ common_sources = ''' systemd_boot_sources = ''' boot.c console.c - shim.c crc32.c + random-seed.c + sha256.c + shim.c '''.split() stub_sources = ''' diff --git a/src/boot/efi/random-seed.c b/src/boot/efi/random-seed.c new file mode 100644 index 0000000000..2185c7a7c2 --- /dev/null +++ b/src/boot/efi/random-seed.c @@ -0,0 +1,340 @@ +#include +#include + +#include "random-seed.h" +#include "sha256.h" +#include "util.h" +#include "shim.h" + +#define RANDOM_MAX_SIZE_MIN (32U) +#define RANDOM_MAX_SIZE_MAX (32U*1024U) + +static const EFI_GUID rng_protocol_guid = EFI_RNG_PROTOCOL_GUID; + +/* SHA256 gives us 256/8=32 bytes */ +#define HASH_VALUE_SIZE 32 + +static EFI_STATUS acquire_rng(UINTN size, VOID **ret) { + _cleanup_freepool_ VOID *data = NULL; + EFI_RNG_PROTOCOL *rng; + EFI_STATUS err; + + /* Try to acquire the specified number of bytes from the UEFI RNG */ + + err = LibLocateProtocol((EFI_GUID*) &rng_protocol_guid, (VOID**) &rng); + if (EFI_ERROR(err)) { + Print(L"Failed to acquire RNG protocol: %r\n", err); + return err; + } + if (!rng) { + /* Print(L"RNG protocol not available.\n"); */ + return EFI_UNSUPPORTED; + } + + data = AllocatePool(size); + if (!data) + return log_oom(); + + err = uefi_call_wrapper(rng->GetRNG, 3, rng, NULL, size, data); + if (EFI_ERROR(err)) { + Print(L"Failed to acquire RNG data: %r\n", err); + return err; + } + + *ret = TAKE_PTR(data); + return EFI_SUCCESS; +} + +static VOID hash_once( + const VOID *old_seed, + const VOID *rng, + UINTN size, + const VOID *system_token, + UINTN system_token_size, + UINTN counter, + UINT8 ret[static HASH_VALUE_SIZE]) { + + /* This hashes together: + * + * 1. The contents of the old seed file + * 2. Some random data acquired from the UEFI RNG (optional) + * 3. Some 'system token' the installer installed as EFI variable (optional) + * 4. A counter value + * + * And writes the result to the specified buffer. + */ + + struct sha256_ctx hash; + + sha256_init_ctx(&hash); + sha256_process_bytes(old_seed, size, &hash); + if (rng) + sha256_process_bytes(rng, size, &hash); + if (system_token_size > 0) + sha256_process_bytes(system_token, system_token_size, &hash); + sha256_process_bytes(&counter, sizeof(counter), &hash); + sha256_finish_ctx(&hash, ret); +} + +static EFI_STATUS hash_many( + const VOID *old_seed, + const VOID *rng, + UINTN size, + const VOID *system_token, + UINTN system_token_size, + UINTN counter_start, + UINTN n, + VOID **ret) { + + _cleanup_freepool_ VOID *output = NULL; + UINTN i; + + /* Hashes the specified parameters in counter mode, generating n hash values, with the counter in the + * range counter_start…counter_start+n-1. */ + + output = AllocatePool(n * HASH_VALUE_SIZE); + if (!output) + return log_oom(); + + for (i = 0; i < n; i++) + hash_once(old_seed, rng, size, + system_token, system_token_size, + counter_start + i, + (UINT8*) output + (i * HASH_VALUE_SIZE)); + + *ret = TAKE_PTR(output); + return EFI_SUCCESS; +} + +static EFI_STATUS mangle_random_seed( + const VOID *old_seed, + const VOID *rng, + UINTN size, + const VOID *system_token, + UINTN system_token_size, + VOID **ret_new_seed, + VOID **ret_for_kernel) { + + _cleanup_freepool_ VOID *new_seed = NULL, *for_kernel = NULL; + EFI_STATUS err; + UINTN n; + + /* This takes the old seed file contents, an (optional) random number acquired from the UEFI RNG, an + * (optional) system 'token' installed once by the OS installer in an EFI variable, and hashes them + * together in counter mode, generating a new seed (to replace the file on disk) and the seed for the + * kernel. To keep things simple, the new seed and kernel data have the same size as the old seed and + * RNG data. */ + + n = (size + HASH_VALUE_SIZE - 1) / HASH_VALUE_SIZE; + + /* Begin hashing in counter mode at counter 0 for the new seed for the disk */ + err = hash_many(old_seed, rng, size, system_token, system_token_size, 0, n, &new_seed); + if (EFI_ERROR(err)) + return err; + + /* Continue counting at 'n' for the seed for the kernel */ + err = hash_many(old_seed, rng, size, system_token, system_token_size, n, n, &for_kernel); + if (EFI_ERROR(err)) + return err; + + *ret_new_seed = TAKE_PTR(new_seed); + *ret_for_kernel = TAKE_PTR(for_kernel); + + return EFI_SUCCESS; +} + +EFI_STATUS acquire_system_token(VOID **ret, UINTN *ret_size) { + _cleanup_freepool_ CHAR8 *data = NULL; + EFI_STATUS err; + UINTN size; + + err = efivar_get_raw(&loader_guid, L"LoaderSystemToken", &data, &size); + if (EFI_ERROR(err)) { + if (err != EFI_NOT_FOUND) + Print(L"Failed to read LoaderSystemToken EFI variable: %r", err); + return err; + } + + if (size <= 0) { + Print(L"System token too short, ignoring."); + return EFI_NOT_FOUND; + } + + *ret = TAKE_PTR(data); + *ret_size = size; + + return EFI_SUCCESS; +} + +static VOID validate_sha256(void) { + +#ifndef __OPTIMIZE__ + /* Let's validate our SHA256 implementation. We stole it from glibc, and converted it to UEFI + * style. We better check whether it does the right stuff. We use the simpler test vectors from the + * SHA spec. Note that we strip this out in optimization builds. */ + + static const struct { + const char *string; + uint8_t hash[HASH_VALUE_SIZE]; + } array[] = { + { "abc", + { 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, + 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, + 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad }}, + + { "", + { 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55 }}, + + { "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + { 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8, + 0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, 0x60, 0x39, + 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67, + 0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1 }}, + + { "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + { 0xcf, 0x5b, 0x16, 0xa7, 0x78, 0xaf, 0x83, 0x80, + 0x03, 0x6c, 0xe5, 0x9e, 0x7b, 0x04, 0x92, 0x37, + 0x0b, 0x24, 0x9b, 0x11, 0xe8, 0xf0, 0x7a, 0x51, + 0xaf, 0xac, 0x45, 0x03, 0x7a, 0xfe, 0xe9, 0xd1 }}, + }; + + UINTN i; + + for (i = 0; i < ELEMENTSOF(array); i++) { + struct sha256_ctx hash; + uint8_t result[HASH_VALUE_SIZE]; + + sha256_init_ctx(&hash); + sha256_process_bytes(array[i].string, strlena((const CHAR8*) array[i].string), &hash); + sha256_finish_ctx(&hash, result); + + if (CompareMem(result, array[i].hash, HASH_VALUE_SIZE) != 0) { + Print(L"SHA256 failed validation.\n"); + uefi_call_wrapper(BS->Stall, 1, 120 * 1000 * 1000); + return; + } + } + + Print(L"SHA256 validated\n"); +#endif +} + +EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode) { + _cleanup_freepool_ VOID *seed = NULL, *new_seed = NULL, *rng = NULL, *for_kernel = NULL, *system_token = NULL; + _cleanup_(FileHandleClosep) EFI_FILE_HANDLE handle = NULL; + UINTN size, rsize, wsize, system_token_size = 0; + _cleanup_freepool_ EFI_FILE_INFO *info = NULL; + EFI_STATUS err; + + validate_sha256(); + + if (mode == RANDOM_SEED_OFF) { + /* Print(L"Random seed handling turned off.\n"); */ + return EFI_NOT_FOUND; + } + + /* Let's better be safe than sorry, and for now disable this logic in SecureBoot mode, so that we + * don't credit a random seed that is not authenticated. */ + if (secure_boot_enabled()) { + /* Print(L"Not loading random seed, because we are in SecureBoot mode.\n"); */ + return EFI_NOT_FOUND; + } + + /* Get some system specific seed that the installer might have placed in an EFI variable. We include + * it in our hash. This is protection against golden master image sloppiness, and it remains on the + * system, even when disk images are duplicated or swapped out. */ + err = acquire_system_token(&system_token, &system_token_size); + if (mode != RANDOM_SEED_ALWAYS) { + /* if (err == EFI_NOT_FOUND) */ + /* Print(L"Not loading random seed, because no system token is set.\n"); */ + if (EFI_ERROR(err)) + return err; /* in all other error cases we already logged */ + } + + err = uefi_call_wrapper(root_dir->Open, 5, root_dir, &handle, L"\\loader\\random-seed", EFI_FILE_MODE_READ|EFI_FILE_MODE_WRITE, 0ULL); + if (EFI_ERROR(err)) { + if (err != EFI_NOT_FOUND) + Print(L"Failed to open random seed file: %r\n", err); + /* else */ + /* Print(L"Not loading random seed, because there is none.\n"); */ + + return err; + } + + info = LibFileInfo(handle); + if (!info) + return log_oom(); + + size = info->FileSize; + if (size < RANDOM_MAX_SIZE_MIN) { + Print(L"Random seed file is too short?\n"); + return EFI_INVALID_PARAMETER; + } + + if (size > RANDOM_MAX_SIZE_MAX) { + Print(L"Random seed file is too large?\n"); + return EFI_INVALID_PARAMETER; + } + + seed = AllocatePool(size); + if (!seed) + return log_oom(); + + rsize = size; + err = uefi_call_wrapper(handle->Read, 3, handle, &rsize, seed); + if (EFI_ERROR(err)) { + Print(L"Failed to read random seed file: %r\n", err); + return err; + } + if (rsize != size) { + Print(L"Short read on random seed file\n"); + return EFI_PROTOCOL_ERROR; + } + + err = uefi_call_wrapper(handle->SetPosition, 2, handle, 0); + if (EFI_ERROR(err)) { + Print(L"Failed to seek to beginning of random seed file: %r\n", err); + return err; + } + + /* Request some random data from the UEFI RNG. We don't need this to work safely, but it's a good + * idea to use it because it helps us for cases where users mistakenly include a random seed in + * golden master images that are replicated many times. */ + (VOID) acquire_rng(size, &rng); /* It's fine if this fails */ + + /* Calculate new random seed for the disk and what to pass to the kernel */ + err = mangle_random_seed(seed, rng, size, system_token, system_token_size, &new_seed, &for_kernel); + if (EFI_ERROR(err)) + return err; + + /* Update the random seed on disk before we use it */ + wsize = size; + err = uefi_call_wrapper(handle->Write, 3, handle, &wsize, new_seed); + if (EFI_ERROR(err)) { + Print(L"Failed to write random seed file: %r\n", err); + return err; + } + if (wsize != size) { + Print(L"Short write on random seed file\n"); + return EFI_PROTOCOL_ERROR; + } + + err = uefi_call_wrapper(handle->Flush, 1, handle); + if (EFI_ERROR(err)) { + Print(L"Failed to flush random seed file: %r\n"); + return err; + } + + /* We are good to go */ + err = efivar_set_raw(&loader_guid, L"LoaderRandomSeed", for_kernel, size, FALSE); + if (EFI_ERROR(err)) { + Print(L"Failed to write random seed to EFI variable: %r\n", err); + return err; + } + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/random-seed.h b/src/boot/efi/random-seed.h new file mode 100644 index 0000000000..0f750716fd --- /dev/null +++ b/src/boot/efi/random-seed.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +typedef enum RandomSeedMode { + RANDOM_SEED_OFF, + RANDOM_SEED_WITH_SYSTEM_TOKEN, + RANDOM_SEED_ALWAYS, + _RANDOM_SEED_MODE_MAX, + _RANDOM_SEED_MODE_INVALID = -1, +} RandomSeedMode; + +EFI_STATUS process_random_seed(EFI_FILE *root_dir, RandomSeedMode mode); diff --git a/src/boot/efi/sha256.c b/src/boot/efi/sha256.c new file mode 100644 index 0000000000..40da636dc7 --- /dev/null +++ b/src/boot/efi/sha256.c @@ -0,0 +1,275 @@ +/* Stolen from glibc and converted to UEFI style. In glibc it comes with the following copyright blurb: */ + +/* Functions to compute SHA256 message digest of files or memory blocks. + according to the definition of SHA256 in FIPS 180-2. + Copyright (C) 2007-2019 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +/* Written by Ulrich Drepper , 2007. */ + +#include "sha256.h" + +#if __BYTE_ORDER == __LITTLE_ENDIAN +# define SWAP(n) \ + (((n) << 24) | (((n) & 0xff00) << 8) | (((n) >> 8) & 0xff00) | ((n) >> 24)) +# define SWAP64(n) \ + (((n) << 56) \ + | (((n) & 0xff00) << 40) \ + | (((n) & 0xff0000) << 24) \ + | (((n) & 0xff000000) << 8) \ + | (((n) >> 8) & 0xff000000) \ + | (((n) >> 24) & 0xff0000) \ + | (((n) >> 40) & 0xff00) \ + | ((n) >> 56)) +#else +# define SWAP(n) (n) +# define SWAP64(n) (n) +#endif + +/* This array contains the bytes used to pad the buffer to the next + 64-byte boundary. (FIPS 180-2:5.1.1) */ +static const UINT8 fillbuf[64] = { + 0x80, 0 /* , 0, 0, ... */ +}; + +/* Constants for SHA256 from FIPS 180-2:4.2.2. */ +static const UINT32 K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +static void sha256_process_block(const void *, UINTN, struct sha256_ctx *); + +/* Initialize structure containing state of computation. + (FIPS 180-2:5.3.2) */ +void sha256_init_ctx(struct sha256_ctx *ctx) { + ctx->H[0] = 0x6a09e667; + ctx->H[1] = 0xbb67ae85; + ctx->H[2] = 0x3c6ef372; + ctx->H[3] = 0xa54ff53a; + ctx->H[4] = 0x510e527f; + ctx->H[5] = 0x9b05688c; + ctx->H[6] = 0x1f83d9ab; + ctx->H[7] = 0x5be0cd19; + + ctx->total64 = 0; + ctx->buflen = 0; +} + +/* Process the remaining bytes in the internal buffer and the usual + prolog according to the standard and write the result to RESBUF. + + IMPORTANT: On some systems it is required that RESBUF is correctly + aligned for a 32 bits value. */ +void *sha256_finish_ctx(struct sha256_ctx *ctx, void *resbuf) { + /* Take yet unprocessed bytes into account. */ + UINT32 bytes = ctx->buflen; + UINTN pad, i; + + /* Now count remaining bytes. */ + ctx->total64 += bytes; + + pad = bytes >= 56 ? 64 + 56 - bytes : 56 - bytes; + CopyMem (&ctx->buffer[bytes], fillbuf, pad); + + /* Put the 64-bit file length in *bits* at the end of the buffer. */ + ctx->buffer32[(bytes + pad + 4) / 4] = SWAP (ctx->total[TOTAL64_low] << 3); + ctx->buffer32[(bytes + pad) / 4] = SWAP ((ctx->total[TOTAL64_high] << 3) + | (ctx->total[TOTAL64_low] >> 29)); + + /* Process last bytes. */ + sha256_process_block (ctx->buffer, bytes + pad + 8, ctx); + + /* Put result from CTX in first 32 bytes following RESBUF. */ + for (i = 0; i < 8; ++i) + ((UINT32 *) resbuf)[i] = SWAP (ctx->H[i]); + + return resbuf; +} + +void sha256_process_bytes(const void *buffer, UINTN len, struct sha256_ctx *ctx) { + /* When we already have some bits in our internal buffer concatenate + both inputs first. */ + + if (ctx->buflen != 0) { + UINTN left_over = ctx->buflen; + UINTN add = 128 - left_over > len ? len : 128 - left_over; + + CopyMem (&ctx->buffer[left_over], buffer, add); + ctx->buflen += add; + + if (ctx->buflen > 64) { + sha256_process_block (ctx->buffer, ctx->buflen & ~63, ctx); + + ctx->buflen &= 63; + /* The regions in the following copy operation cannot overlap. */ + CopyMem (ctx->buffer, &ctx->buffer[(left_over + add) & ~63], + ctx->buflen); + } + + buffer = (const char *) buffer + add; + len -= add; + } + + /* Process available complete blocks. */ + if (len >= 64) { +#if !_STRING_ARCH_unaligned +/* To check alignment gcc has an appropriate operator. Other + compilers don't. */ +# if __GNUC__ >= 2 +# define UNALIGNED_P(p) (((UINTN) p) % __alignof__ (UINT32) != 0) +# else +# define UNALIGNED_P(p) (((UINTN) p) % sizeof (UINT32) != 0) +# endif + if (UNALIGNED_P (buffer)) + while (len > 64) { + CopyMem (ctx->buffer, buffer, 64); + sha256_process_block (ctx->buffer, 64, ctx); + buffer = (const char *) buffer + 64; + len -= 64; + } + else +#endif + { + sha256_process_block (buffer, len & ~63, ctx); + buffer = (const char *) buffer + (len & ~63); + len &= 63; + } + } + + /* Move remaining bytes into internal buffer. */ + if (len > 0) { + UINTN left_over = ctx->buflen; + + CopyMem (&ctx->buffer[left_over], buffer, len); + left_over += len; + if (left_over >= 64) { + sha256_process_block (ctx->buffer, 64, ctx); + left_over -= 64; + CopyMem (ctx->buffer, &ctx->buffer[64], left_over); + } + ctx->buflen = left_over; + } +} + + +/* Process LEN bytes of BUFFER, accumulating context into CTX. + It is assumed that LEN % 64 == 0. */ +static void sha256_process_block(const void *buffer, UINTN len, struct sha256_ctx *ctx) { + const UINT32 *words = buffer; + UINTN nwords = len / sizeof (UINT32); + UINT32 a = ctx->H[0]; + UINT32 b = ctx->H[1]; + UINT32 c = ctx->H[2]; + UINT32 d = ctx->H[3]; + UINT32 e = ctx->H[4]; + UINT32 f = ctx->H[5]; + UINT32 g = ctx->H[6]; + UINT32 h = ctx->H[7]; + + /* First increment the byte count. FIPS 180-2 specifies the possible + length of the file up to 2^64 bits. Here we only compute the + number of bytes. */ + ctx->total64 += len; + + /* Process all bytes in the buffer with 64 bytes in each round of + the loop. */ + while (nwords > 0) { + UINT32 W[64]; + UINT32 a_save = a; + UINT32 b_save = b; + UINT32 c_save = c; + UINT32 d_save = d; + UINT32 e_save = e; + UINT32 f_save = f; + UINT32 g_save = g; + UINT32 h_save = h; + UINTN t; + + /* Operators defined in FIPS 180-2:4.1.2. */ +#define Ch(x, y, z) ((x & y) ^ (~x & z)) +#define Maj(x, y, z) ((x & y) ^ (x & z) ^ (y & z)) +#define S0(x) (CYCLIC (x, 2) ^ CYCLIC (x, 13) ^ CYCLIC (x, 22)) +#define S1(x) (CYCLIC (x, 6) ^ CYCLIC (x, 11) ^ CYCLIC (x, 25)) +#define R0(x) (CYCLIC (x, 7) ^ CYCLIC (x, 18) ^ (x >> 3)) +#define R1(x) (CYCLIC (x, 17) ^ CYCLIC (x, 19) ^ (x >> 10)) + + /* It is unfortunate that C does not provide an operator for + cyclic rotation. Hope the C compiler is smart enough. */ +#define CYCLIC(w, s) ((w >> s) | (w << (32 - s))) + + /* Compute the message schedule according to FIPS 180-2:6.2.2 step 2. */ + for (t = 0; t < 16; ++t) { + W[t] = SWAP (*words); + ++words; + } + for (t = 16; t < 64; ++t) + W[t] = R1 (W[t - 2]) + W[t - 7] + R0 (W[t - 15]) + W[t - 16]; + + /* The actual computation according to FIPS 180-2:6.2.2 step 3. */ + for (t = 0; t < 64; ++t) { + UINT32 T1 = h + S1 (e) + Ch (e, f, g) + K[t] + W[t]; + UINT32 T2 = S0 (a) + Maj (a, b, c); + h = g; + g = f; + f = e; + e = d + T1; + d = c; + c = b; + b = a; + a = T1 + T2; + } + + /* Add the starting values of the context according to FIPS 180-2:6.2.2 + step 4. */ + a += a_save; + b += b_save; + c += c_save; + d += d_save; + e += e_save; + f += f_save; + g += g_save; + h += h_save; + + /* Prepare for the next round. */ + nwords -= 16; + } + + /* Put checksum in context given as argument. */ + ctx->H[0] = a; + ctx->H[1] = b; + ctx->H[2] = c; + ctx->H[3] = d; + ctx->H[4] = e; + ctx->H[5] = f; + ctx->H[6] = g; + ctx->H[7] = h; +} diff --git a/src/boot/efi/sha256.h b/src/boot/efi/sha256.h new file mode 100644 index 0000000000..ca9f285937 --- /dev/null +++ b/src/boot/efi/sha256.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +struct sha256_ctx { + UINT32 H[8]; + + union { + UINT64 total64; +#define TOTAL64_low (1 - (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) +#define TOTAL64_high (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + UINT32 total[2]; + }; + + UINT32 buflen; + + union { + UINT8 buffer[128]; /* NB: always correctly aligned for UINT32. */ + UINT32 buffer32[32]; + UINT64 buffer64[16]; + }; +}; + +void sha256_init_ctx(struct sha256_ctx *ctx); +void *sha256_finish_ctx(struct sha256_ctx *ctx, VOID *resbuf); +void sha256_process_bytes(const void *buffer, UINTN len, struct sha256_ctx *ctx); diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c index 4134e2200a..da743dcb9f 100644 --- a/src/boot/efi/util.c +++ b/src/boot/efi/util.c @@ -309,8 +309,8 @@ CHAR8 *strchra(CHAR8 *s, CHAR8 c) { return NULL; } -EFI_STATUS file_read(EFI_FILE_HANDLE dir, const CHAR16 *name, UINTN off, UINTN size, CHAR8 **content, UINTN *content_size) { - EFI_FILE_HANDLE handle; +EFI_STATUS file_read(EFI_FILE_HANDLE dir, const CHAR16 *name, UINTN off, UINTN size, CHAR8 **ret, UINTN *ret_size) { + _cleanup_(FileHandleClosep) EFI_FILE_HANDLE handle = NULL; _cleanup_freepool_ CHAR8 *buf = NULL; EFI_STATUS err; @@ -322,6 +322,9 @@ EFI_STATUS file_read(EFI_FILE_HANDLE dir, const CHAR16 *name, UINTN off, UINTN s _cleanup_freepool_ EFI_FILE_INFO *info; info = LibFileInfo(handle); + if (!info) + return EFI_OUT_OF_RESOURCES; + size = info->FileSize+1; } @@ -332,15 +335,24 @@ EFI_STATUS file_read(EFI_FILE_HANDLE dir, const CHAR16 *name, UINTN off, UINTN s } buf = AllocatePool(size + 1); + if (!buf) + return EFI_OUT_OF_RESOURCES; + err = uefi_call_wrapper(handle->Read, 3, handle, &size, buf); - if (!EFI_ERROR(err)) { - buf[size] = '\0'; - *content = buf; - buf = NULL; - if (content_size) - *content_size = size; - } - uefi_call_wrapper(handle->Close, 1, handle); + if (EFI_ERROR(err)) + return err; + + buf[size] = '\0'; + + *ret = TAKE_PTR(buf); + if (ret_size) + *ret_size = size; return err; } + +EFI_STATUS log_oom(void) { + Print(L"Out of memory."); + (void) uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return EFI_OUT_OF_RESOURCES; +} diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h index cef127f400..8518dfde77 100644 --- a/src/boot/efi/util.h +++ b/src/boot/efi/util.h @@ -66,3 +66,5 @@ const EFI_GUID loader_guid; (ptr) = NULL; \ _ptr_; \ }) + +EFI_STATUS log_oom(void); diff --git a/src/core/efi-random.c b/src/core/efi-random.c new file mode 100644 index 0000000000..c4d25d68e4 --- /dev/null +++ b/src/core/efi-random.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include + +#include "alloc-util.h" +#include "chattr-util.h" +#include "efi-random.h" +#include "efivars.h" +#include "fd-util.h" +#include "fs-util.h" +#include "strv.h" + +/* If a random seed was passed by the boot loader in the LoaderRandomSeed EFI variable, let's credit it to + * the kernel's random pool, but only once per boot. If this is run very early during initialization we can + * instantly boot up with a filled random pool. + * + * This makes no judgement on the entropy passed, it's the job of the boot loader to only pass us a seed that + * is suitably validated. */ + +static void lock_down_efi_variables(void) { + const char *p; + int r; + + /* Paranoia: let's restrict access modes of these a bit, so that unprivileged users can't use them to + * identify the system or gain too much insight into what we might have credited to the entropy + * pool. */ + FOREACH_STRING(p, + "/sys/firmware/efi/efivars/LoaderRandomSeed-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", + "/sys/firmware/efi/efivars/LoaderSystemToken-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f") { + + r = chattr_path(p, 0, FS_IMMUTABLE_FL, NULL); + if (r == -ENOENT) + continue; + if (r < 0) + log_warning_errno(r, "Failed to drop FS_IMMUTABLE_FL from %s, ignoring: %m", p); + + if (chmod(p, 0600) < 0) + log_warning_errno(errno, "Failed to reduce access mode of %s, ignoring: %m", p); + } +} + +int efi_take_random_seed(void) { + _cleanup_free_ struct rand_pool_info *info = NULL; + _cleanup_free_ void *value = NULL; + _cleanup_close_ int random_fd = -1; + size_t size; + int r; + + /* Paranoia comes first. */ + lock_down_efi_variables(); + + if (access("/run/systemd/efi-random-seed-taken", F_OK) < 0) { + if (errno != ENOENT) { + log_warning_errno(errno, "Failed to determine whether we already used the random seed token, not using it."); + return 0; + } + + /* ENOENT means we haven't used it yet. */ + } else { + log_debug("EFI random seed already used, not using again."); + return 0; + } + + r = efi_get_variable(EFI_VENDOR_LOADER, "LoaderRandomSeed", NULL, &value, &size); + if (r == -EOPNOTSUPP) { + log_debug_errno(r, "System lacks EFI support, not initializing random seed from EFI variable."); + return 0; + } + if (r == -ENOENT) { + log_debug_errno(r, "Boot loader did not pass LoaderRandomSeed EFI variable, not crediting any entropy."); + return 0; + } + if (r < 0) + return log_warning_errno(r, "Failed to read LoaderRandomSeed EFI variable, ignoring: %m"); + + if (size == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Random seed passed from boot loader has zero size? Ignoring."); + + /* The kernel API only accepts "int" as entropy count (which is in bits), let's avoid any chance for + * confusion here. */ + if (size > INT_MAX / 8) + size = INT_MAX / 8; + + random_fd = open("/dev/urandom", O_WRONLY|O_CLOEXEC|O_NOCTTY); + if (random_fd < 0) + return log_warning_errno(errno, "Failed to open /dev/urandom for writing, ignoring: %m"); + + /* Before we use the seed, let's mark it as used, so that we never credit it twice. Also, it's a nice + * way to let users known that we successfully acquired entropy from the boot laoder. */ + r = touch("/run/systemd/efi-random-seed-taken"); + if (r < 0) + return log_warning_errno(r, "Unable to mark EFI random seed as used, not using it: %m"); + + info = malloc(offsetof(struct rand_pool_info, buf) + size); + if (!info) + return log_oom(); + + info->entropy_count = size * 8; + info->buf_size = size; + memcpy(info->buf, value, size); + + if (ioctl(random_fd, RNDADDENTROPY, info) < 0) + return log_warning_errno(errno, "Failed to credit entropy, ignoring: %m"); + + log_info("Successfully credited entropy passed from boot loader."); + return 1; +} diff --git a/src/core/efi-random.h b/src/core/efi-random.h new file mode 100644 index 0000000000..c1de8671d8 --- /dev/null +++ b/src/core/efi-random.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +int efi_take_random_seed(void); diff --git a/src/core/main.c b/src/core/main.c index ef17e71e11..0674e00ab0 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -32,9 +32,10 @@ #include "clock-util.h" #include "conf-parser.h" #include "cpu-set-util.h" -#include "dbus.h" #include "dbus-manager.h" +#include "dbus.h" #include "def.h" +#include "efi-random.h" #include "emergency-action.h" #include "env-util.h" #include "exit-status.h" @@ -2511,6 +2512,9 @@ int main(int argc, char *argv[]) { error_message = "Failed to mount API filesystems"; goto finish; } + + /* The efivarfs is now mounted, let's read the random seed off it */ + (void) efi_take_random_seed(); } /* Save the original RLIMIT_NOFILE/RLIMIT_MEMLOCK so that we can reset it later when diff --git a/src/core/meson.build b/src/core/meson.build index 34762dbc21..267d65a3b2 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -66,6 +66,8 @@ libcore_sources = ''' device.h dynamic-user.c dynamic-user.h + efi-random.c + efi-random.h emergency-action.c emergency-action.h execute.c diff --git a/src/random-seed/random-seed.c b/src/random-seed/random-seed.c index 510a2715f2..cbc9745584 100644 --- a/src/random-seed/random-seed.c +++ b/src/random-seed/random-seed.c @@ -2,8 +2,14 @@ #include #include +#include #include +#include +#if USE_SYS_RANDOM_H +# include +#endif #include +#include #include #include "sd-id128.h" @@ -14,21 +20,94 @@ #include "io-util.h" #include "log.h" #include "main-func.h" +#include "missing.h" #include "mkdir.h" +#include "parse-util.h" +#include "random-util.h" #include "string-util.h" #include "util.h" +#include "xattr-util.h" -#define POOL_SIZE_MIN 512 -#define POOL_SIZE_MAX (10*1024*1024) +typedef enum CreditEntropy { + CREDIT_ENTROPY_NO_WAY, + CREDIT_ENTROPY_YES_PLEASE, + CREDIT_ENTROPY_YES_FORCED, +} CreditEntropy; + +static CreditEntropy may_credit(int seed_fd) { + _cleanup_free_ char *creditable = NULL; + const char *e; + int r; + + assert(seed_fd >= 0); + + e = getenv("SYSTEMD_RANDOM_SEED_CREDIT"); + if (!e) { + log_debug("$SYSTEMD_RANDOM_SEED_CREDIT is not set, not crediting entropy."); + return CREDIT_ENTROPY_NO_WAY; + } + if (streq(e, "force")) { + log_debug("$SYSTEMD_RANDOM_SEED_CREDIT is set to 'force', crediting entropy."); + return CREDIT_ENTROPY_YES_FORCED; + } + + r = parse_boolean(e); + if (r <= 0) { + if (r < 0) + log_warning_errno(r, "Failed to parse $SYSTEMD_RANDOM_SEED_CREDIT, not crediting entropy: %m"); + else + log_debug("Crediting entropy is turned off via $SYSTEMD_RANDOM_SEED_CREDIT, not crediting entropy."); + + return CREDIT_ENTROPY_NO_WAY; + } + + /* Determine if the file is marked as creditable */ + r = fgetxattr_malloc(seed_fd, "user.random-seed-creditable", &creditable); + if (r < 0) { + if (IN_SET(r, -ENODATA, -ENOSYS, -EOPNOTSUPP)) + log_debug_errno(r, "Seed file is not marked as creditable, not crediting."); + else + log_warning_errno(r, "Failed to read extended attribute, ignoring: %m"); + + return CREDIT_ENTROPY_NO_WAY; + } + + r = parse_boolean(creditable); + if (r <= 0) { + if (r < 0) + log_warning_errno(r, "Failed to parse user.random-seed-creditable extended attribute, ignoring: %s", creditable); + else + log_debug("Seed file is marked as not creditable, not crediting."); + + return CREDIT_ENTROPY_NO_WAY; + } + + /* Don't credit the random seed if we are in first-boot mode, because we are supposed to start from + * scratch. This is a safety precaution for cases where we people ship "golden" images with empty + * /etc but populated /var that contains a random seed. */ + if (access("/run/systemd/first-boot", F_OK) < 0) { + + if (errno != ENOENT) { + log_warning_errno(errno, "Failed to check whether we are in first-boot mode, not crediting entropy: %m"); + return CREDIT_ENTROPY_NO_WAY; + } + + /* If ENOENT all is good, we are not in first-boot mode. */ + } else { + log_debug("Not crediting entropy, since booted in first-boot mode."); + return CREDIT_ENTROPY_NO_WAY; + } + + return CREDIT_ENTROPY_YES_PLEASE; +} static int run(int argc, char *argv[]) { _cleanup_close_ int seed_fd = -1, random_fd = -1; - bool read_seed_file, write_seed_file; + bool read_seed_file, write_seed_file, synchronous; _cleanup_free_ void* buf = NULL; - size_t buf_size = 0; + size_t buf_size; struct stat st; ssize_t k; - FILE *f; int r; log_setup_service(); @@ -39,18 +118,7 @@ static int run(int argc, char *argv[]) { umask(0022); - /* Read pool size, if possible */ - f = fopen("/proc/sys/kernel/random/poolsize", "re"); - if (f) { - if (fscanf(f, "%zu", &buf_size) > 0) - /* poolsize is in bits on 2.6, but we want bytes */ - buf_size /= 8; - - fclose(f); - } - - if (buf_size < POOL_SIZE_MIN) - buf_size = POOL_SIZE_MIN; + buf_size = random_pool_size(); r = mkdir_parents(RANDOM_SEED, 0755); if (r < 0) @@ -60,11 +128,11 @@ static int run(int argc, char *argv[]) { * new data, to make sure the next boot gets seeded differently. */ if (streq(argv[1], "load")) { - int open_rw_error; seed_fd = open(RANDOM_SEED, O_RDWR|O_CLOEXEC|O_NOCTTY|O_CREAT, 0600); - open_rw_error = -errno; if (seed_fd < 0) { + int open_rw_error = -errno; + write_seed_file = false; seed_fd = open(RANDOM_SEED, O_RDONLY|O_CLOEXEC|O_NOCTTY); @@ -81,15 +149,11 @@ static int run(int argc, char *argv[]) { write_seed_file = true; random_fd = open("/dev/urandom", O_RDWR|O_CLOEXEC|O_NOCTTY, 0600); - if (random_fd < 0) { - write_seed_file = false; - - random_fd = open("/dev/urandom", O_WRONLY|O_CLOEXEC|O_NOCTTY, 0600); - if (random_fd < 0) - return log_error_errno(errno, "Failed to open /dev/urandom: %m"); - } + if (random_fd < 0) + return log_error_errno(errno, "Failed to open /dev/urandom: %m"); read_seed_file = true; + synchronous = true; /* make this invocation a synchronous barrier for random pool initialization */ } else if (streq(argv[1], "save")) { @@ -103,7 +167,7 @@ static int run(int argc, char *argv[]) { read_seed_file = false; write_seed_file = true; - + synchronous = false; } else return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]); @@ -113,7 +177,7 @@ static int run(int argc, char *argv[]) { /* If the seed file is larger than what we expect, then honour the existing size and save/restore as much as it says */ if ((uint64_t) st.st_size > buf_size) - buf_size = MIN(st.st_size, POOL_SIZE_MAX); + buf_size = MIN(st.st_size, RANDOM_POOL_SIZE_MAX); buf = malloc(buf_size); if (!buf) @@ -121,57 +185,131 @@ static int run(int argc, char *argv[]) { if (read_seed_file) { sd_id128_t mid; - int z; + + /* First, let's write the machine ID into /dev/urandom, not crediting entropy. Why? As an + * extra protection against "golden images" that are put together sloppily, i.e. images which + * are duplicated on multiple systems but where the random seed file is not properly + * reset. Frequently the machine ID is properly reset on those systems however (simply + * because it's easier to notice, if it isn't due to address clashes and so on, while random + * seed equivalence is generally not noticed easily), hence let's simply write the machined + * ID into the random pool too. */ + r = sd_id128_get_machine(&mid); + if (r < 0) + log_debug_errno(r, "Failed to get machine ID, ignoring: %m"); + else { + r = loop_write(random_fd, &mid, sizeof(mid), false); + if (r < 0) + log_debug_errno(r, "Failed to write machine ID to /dev/urandom, ignoring: %m"); + } k = loop_read(seed_fd, buf, buf_size, false); if (k < 0) - r = log_error_errno(k, "Failed to read seed from " RANDOM_SEED ": %m"); - else if (k == 0) { - r = 0; + log_error_errno(k, "Failed to read seed from " RANDOM_SEED ": %m"); + else if (k == 0) log_debug("Seed file " RANDOM_SEED " not yet initialized, proceeding."); - } else { + else { + CreditEntropy lets_credit; + (void) lseek(seed_fd, 0, SEEK_SET); - r = loop_write(random_fd, buf, (size_t) k, false); - if (r < 0) - log_error_errno(r, "Failed to write seed to /dev/urandom: %m"); - } + lets_credit = may_credit(seed_fd); - /* Let's also write the machine ID into the random seed. Why? As an extra protection against "golden - * images" that are put together sloppily, i.e. images which are duplicated on multiple systems but - * where the random seed file is not properly reset. Frequently the machine ID is properly reset on - * those systems however (simply because it's easier to notice, if it isn't due to address clashes and - * so on, while random seed equivalence is generally not noticed easily), hence let's simply write the - * machined ID into the random pool too. */ - z = sd_id128_get_machine(&mid); - if (z < 0) - log_debug_errno(z, "Failed to get machine ID, ignoring: %m"); - else { - z = loop_write(random_fd, &mid, sizeof(mid), false); - if (z < 0) - log_debug_errno(z, "Failed to write machine ID to /dev/urandom, ignoring: %m"); + /* Before we credit or use the entropy, let's make sure to securely drop the + * creditable xattr from the file, so that we never credit the same random seed + * again. Note that further down we'll write a new seed again, and likely mark it as + * credible again, hence this is just paranoia to close the short time window between + * the time we upload the random seed into the kernel and download the new one from + * it. */ + + if (fremovexattr(seed_fd, "user.random-seed-creditable") < 0) { + if (!IN_SET(errno, ENODATA, ENOSYS, EOPNOTSUPP)) + log_warning_errno(errno, "Failed to remove extended attribute, ignoring: %m"); + + /* Otherwise, there was no creditable flag set, which is OK. */ + } else { + r = fsync_full(seed_fd); + if (r < 0) { + log_warning_errno(r, "Failed to synchronize seed to disk, not crediting entropy: %m"); + + if (lets_credit == CREDIT_ENTROPY_YES_PLEASE) + lets_credit = CREDIT_ENTROPY_NO_WAY; + } + } + + if (IN_SET(lets_credit, CREDIT_ENTROPY_YES_PLEASE, CREDIT_ENTROPY_YES_FORCED)) { + _cleanup_free_ struct rand_pool_info *info = NULL; + + info = malloc(offsetof(struct rand_pool_info, buf) + k); + if (!info) + return log_oom(); + + info->entropy_count = k * 8; + info->buf_size = k; + memcpy(info->buf, buf, k); + + if (ioctl(random_fd, RNDADDENTROPY, info) < 0) + return log_warning_errno(errno, "Failed to credit entropy, ignoring: %m"); + } else { + r = loop_write(random_fd, buf, (size_t) k, false); + if (r < 0) + log_error_errno(r, "Failed to write seed to /dev/urandom: %m"); + } } } if (write_seed_file) { - /* This is just a safety measure. Given that we are root and - * most likely created the file ourselves the mode and owner - * should be correct anyway. */ - (void) fchmod_and_chown(seed_fd, 0600, 0, 0); + bool getrandom_worked = false; - k = loop_read(random_fd, buf, buf_size, false); + /* This is just a safety measure. Given that we are root and most likely created the file + * ourselves the mode and owner should be correct anyway. */ + r = fchmod_and_chown(seed_fd, 0600, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to adjust seed file ownership and access mode."); + + /* Let's make this whole job asynchronous, i.e. let's make ourselves a barrier for + * proper initialization of the random pool. */ + k = getrandom(buf, buf_size, GRND_NONBLOCK); + if (k < 0 && errno == EAGAIN && synchronous) { + log_notice("Kernel entropy pool is not initialized yet, waiting until it is."); + k = getrandom(buf, buf_size, 0); /* retry synchronously */ + } if (k < 0) - return log_error_errno(k, "Failed to read new seed from /dev/urandom: %m"); - if (k == 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Got EOF while reading from /dev/urandom."); + log_debug_errno(errno, "Failed to read random data with getrandom(), falling back to /dev/urandom: %m"); + else if ((size_t) k < buf_size) + log_debug("Short read from getrandom(), falling back to /dev/urandom: %m"); + else + getrandom_worked = true; + + if (!getrandom_worked) { + /* Retry with classic /dev/urandom */ + k = loop_read(random_fd, buf, buf_size, false); + if (k < 0) + return log_error_errno(k, "Failed to read new seed from /dev/urandom: %m"); + if (k == 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Got EOF while reading from /dev/urandom."); + } r = loop_write(seed_fd, buf, (size_t) k, false); if (r < 0) return log_error_errno(r, "Failed to write new random seed file: %m"); + + if (ftruncate(seed_fd, k) < 0) + return log_error_errno(r, "Failed to truncate random seed file: %m"); + + r = fsync_full(seed_fd); + if (r < 0) + return log_error_errno(r, "Failed to synchronize seed file: %m"); + + /* If we got this random seed data from getrandom() the data is suitable for crediting + * entropy later on. Let's keep that in mind by setting an extended attribute. on the file */ + if (getrandom_worked) + if (fsetxattr(seed_fd, "user.random-seed-creditable", "1", 1, 0) < 0) + log_full_errno(IN_SET(errno, ENOSYS, EOPNOTSUPP) ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to mark seed file as creditable, ignoring: %m"); } - return r; + return 0; } DEFINE_MAIN_FUNCTION(run); diff --git a/src/shared/efivars.c b/src/shared/efivars.c index a4a3c200ac..3597ddf4a7 100644 --- a/src/shared/efivars.c +++ b/src/shared/efivars.c @@ -207,25 +207,32 @@ char* efi_variable_path(sd_id128_t vendor, const char *name) { int efi_get_variable( sd_id128_t vendor, const char *name, - uint32_t *attribute, - void **value, - size_t *size) { + uint32_t *ret_attribute, + void **ret_value, + size_t *ret_size) { _cleanup_close_ int fd = -1; _cleanup_free_ char *p = NULL; + _cleanup_free_ void *buf = NULL; + struct stat st; uint32_t a; ssize_t n; - struct stat st; - _cleanup_free_ void *buf = NULL; assert(name); - assert(value); - assert(size); p = efi_variable_path(vendor, name); if (!p) return -ENOMEM; + if (!ret_value && !ret_size && !ret_attribute) { + /* If caller is not interested in anything, just check if the variable exists and is readable + * to us. */ + if (access(p, R_OK) < 0) + return -errno; + + return 0; + } + fd = open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC); if (fd < 0) return -errno; @@ -237,31 +244,41 @@ int efi_get_variable( if (st.st_size > 4*1024*1024 + 4) return -E2BIG; - n = read(fd, &a, sizeof(a)); - if (n < 0) - return -errno; - if (n != sizeof(a)) - return -EIO; + if (ret_value || ret_attribute) { + n = read(fd, &a, sizeof(a)); + if (n < 0) + return -errno; + if (n != sizeof(a)) + return -EIO; + } - buf = malloc(st.st_size - 4 + 2); - if (!buf) - return -ENOMEM; + if (ret_value) { + buf = malloc(st.st_size - 4 + 2); + if (!buf) + return -ENOMEM; - n = read(fd, buf, (size_t) st.st_size - 4); - if (n < 0) - return -errno; - if (n != (ssize_t) st.st_size - 4) - return -EIO; + n = read(fd, buf, (size_t) st.st_size - 4); + if (n < 0) + return -errno; + if (n != st.st_size - 4) + return -EIO; - /* Always NUL terminate (2 bytes, to protect UTF-16) */ - ((char*) buf)[st.st_size - 4] = 0; - ((char*) buf)[st.st_size - 4 + 1] = 0; + /* Always NUL terminate (2 bytes, to protect UTF-16) */ + ((char*) buf)[st.st_size - 4] = 0; + ((char*) buf)[st.st_size - 4 + 1] = 0; + } - *value = TAKE_PTR(buf); - *size = (size_t) st.st_size - 4; + /* Note that efivarfs interestingly doesn't require ftruncate() to update an existing EFI variable + * with a smaller value. */ - if (attribute) - *attribute = a; + if (ret_attribute) + *ret_attribute = a; + + if (ret_value) + *ret_value = TAKE_PTR(buf); + + if (ret_size) + *ret_size = (size_t) st.st_size - 4; return 0; } diff --git a/units/meson.build b/units/meson.build index 72535b85ee..5d2f723a70 100644 --- a/units/meson.build +++ b/units/meson.build @@ -135,6 +135,8 @@ in_units = [ 'sysinit.target.wants/'], ['systemd-bless-boot.service', 'ENABLE_EFI HAVE_BLKID'], ['systemd-boot-check-no-failures.service', ''], + ['systemd-boot-system-token.service', 'ENABLE_EFI', + 'sysinit.target.wants/'], ['systemd-coredump@.service', 'ENABLE_COREDUMP'], ['systemd-pstore.service', 'ENABLE_PSTORE'], ['systemd-firstboot.service', 'ENABLE_FIRSTBOOT', diff --git a/units/systemd-boot-system-token.service.in b/units/systemd-boot-system-token.service.in new file mode 100644 index 0000000000..5bc29b0cb7 --- /dev/null +++ b/units/systemd-boot-system-token.service.in @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Store a System Token in an EFI Variable +Documentation=man:systemd-boot-system-token.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=local-fs.target systemd-random-seed.service +Before=shutdown.target + +# Don't run this in a VM environment, because there EFI variables are not +# actually stored in NVRAM, independent of regular storage. +ConditionVirtualization=no + +# Only run this if the boot loader can support random seed initialization. +ConditionPathExists=/sys/firmware/efi/efivars/LoaderFeatures-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f + +# Only run this if there is no system token defined yet, or … +ConditionPathExists=|!/sys/firmware/efi/efivars/LoaderSystemToken-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f + +# … if the boot loader didn't pass the OS a random seed (and thus probably was missing the random seed file) +ConditionPathExists=|!/sys/firmware/efi/efivars/LoaderRandomSeed-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=@bindir@/bootctl random-seed diff --git a/units/systemd-random-seed.service.in b/units/systemd-random-seed.service.in index 8903ee896c..5a490420e9 100644 --- a/units/systemd-random-seed.service.in +++ b/units/systemd-random-seed.service.in @@ -22,4 +22,9 @@ Type=oneshot RemainAfterExit=yes ExecStart=@rootlibexecdir@/systemd-random-seed load ExecStop=@rootlibexecdir@/systemd-random-seed save -TimeoutSec=30s + +# This service waits until the kernel's entropy pool is initialized, and may be +# used as ordering barrier for service that require an initialized entropy +# pool. Since initialization can take a while on entropy-starved systems, let's +# increase the time-out substantially here. +TimeoutSec=10min