diff mbox series

[v5] package/urandom-scripts: actually credit seed files via seedrng

Message ID 20220420170326.1901572-1-Jason@zx2c4.com
State Accepted
Headers show
Series [v5] package/urandom-scripts: actually credit seed files via seedrng | expand

Commit Message

Jason A. Donenfeld April 20, 2022, 5:03 p.m. UTC
The RNG can't actually be seeded from a shell script, due to the
reliance on ioctls. For this reason, Busybox now contains SeedRNG, a
tiny program meant to be called at startup and shutdown (and at
arbitrary other points in between if desired).

Since this is currently in Busybox's git tree, this commit adds it as a
patch to the Busybox package. Then, it wires the init script up to call
it.

This is a significant improvement over the current init script, which
doesn't credit entropy and whose hashing in shell scripts is sort of
fragile.

NOTE NOTE NOTE: While I've added the patch to the Busybox package
directory, I don't really know how to wire this up and what to do about
the various config entries. I was hoping that a Buildroot maintainer
might have a better idea of the best way to do this, and could just take
this patch as a starting point for whatever the real commit winds up
being.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
---
Changes v4->v5:
- SeedRNG is now part of Busybox, so we don't need to carry the source
  in-tree here, which was the primary objection to earlier versions.

 ...eedRNG-utility-for-kernel-RNG-seed-f.patch | 309 ++++++++++++++++++
 package/urandom-scripts/Config.in             |   4 -
 package/urandom-scripts/S20urandom            |  78 ++---
 3 files changed, 330 insertions(+), 61 deletions(-)
 create mode 100644 package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch

Comments

Jason A. Donenfeld April 20, 2022, 5:11 p.m. UTC | #1
And if it's easier to read this not in patch form, Busybox's ittybitty
seedrng implementation lives here:
https://git.busybox.net/busybox/tree/util-linux/seedrng.c
Jason A. Donenfeld April 22, 2022, 3:44 p.m. UTC | #2
Another thought: I noticed the busybox package itself has various init
scripts. We could just delete the now-obsolete urandom-scripts
package, and simply add the init script to the busybox package. Would
this be workable?

Jason
James Hilliard May 2, 2022, 9:32 a.m. UTC | #3
On Wed, Apr 20, 2022 at 11:04 AM Jason A. Donenfeld <Jason@zx2c4.com> wrote:
>
> The RNG can't actually be seeded from a shell script, due to the
> reliance on ioctls. For this reason, Busybox now contains SeedRNG, a
> tiny program meant to be called at startup and shutdown (and at
> arbitrary other points in between if desired).
>
> Since this is currently in Busybox's git tree, this commit adds it as a
> patch to the Busybox package. Then, it wires the init script up to call
> it.
>
> This is a significant improvement over the current init script, which
> doesn't credit entropy and whose hashing in shell scripts is sort of
> fragile.
>
> NOTE NOTE NOTE: While I've added the patch to the Busybox package
> directory, I don't really know how to wire this up and what to do about
> the various config entries. I was hoping that a Buildroot maintainer
> might have a better idea of the best way to do this, and could just take
> this patch as a starting point for whatever the real commit winds up
> being.

Add a kconfig option(ie BR2_PACKAGE_BUSYBOX_SEEDRNG) here:
https://github.com/buildroot/buildroot/blob/master/package/busybox/Config.in

Do something like:
ifeq ($(BR2_PACKAGE_BUSYBOX_SEEDRNG),y)
define BUSYBOX_SET_SEEDRNG
    $(call KCONFIG_ENABLE_OPT,CONFIG_SEEDRNG)
endef
endif
here https://github.com/buildroot/buildroot/blob/master/package/busybox/busybox.mk

Probably also need to make sure that urandom-scripts is only built
if busybox is built as well if unconditionally using seedrng in the script.

Add something like:
depends on BR2_PACKAGE_BUSYBOX
select BR2_PACKAGE_BUSYBOX_SEEDRNG
for BR2_PACKAGE_URANDOM_SCRIPTS
https://github.com/buildroot/buildroot/blob/master/package/urandom-scripts/Config.in

Once there is a normal util-linux variant then busybox would not be
required and that variant could be conditionally selected if busybox
is not enabled I think.

>
> Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
> ---
> Changes v4->v5:
> - SeedRNG is now part of Busybox, so we don't need to carry the source
>   in-tree here, which was the primary objection to earlier versions.
>
>  ...eedRNG-utility-for-kernel-RNG-seed-f.patch | 309 ++++++++++++++++++
>  package/urandom-scripts/Config.in             |   4 -
>  package/urandom-scripts/S20urandom            |  78 ++---
>  3 files changed, 330 insertions(+), 61 deletions(-)
>  create mode 100644 package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch
>
> diff --git a/package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch b/package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch
> new file mode 100644
> index 0000000000..493c31b247
> --- /dev/null
> +++ b/package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch
> @@ -0,0 +1,309 @@
> +From 47660dc9581e251f555ca9b05b630276f1d487ee Mon Sep 17 00:00:00 2001
> +From: "Jason A. Donenfeld" <Jason@zx2c4.com>
> +Date: Mon, 4 Apr 2022 18:21:51 +0200
> +Subject: [PATCH] seedrng: import SeedRNG utility for kernel RNG seed files
> +
> +The RNG can't actually be seeded from a shell script, due to the
> +reliance on ioctls and the fact that entropy written into the
> +unprivileged /dev/urandom device is not immediately mixed in, making
> +subsequent seed reads dangerous. For this reason, the seedrng project
> +provides a basic "C script" meant to be copy and pasted into projects
> +like Busybox and tweaked as needed: <https://git.zx2c4.com/seedrng/about/>.
> +
> +The SeedRNG construction has been part of systemd's seeder since
> +January, and recently was added to Android, OpenRC, and Void's Runit,
> +with more integrations on their way depending on context. Virtually
> +every single Busybox-based distro I have seen seeds things in wrong,
> +incomplete, or otherwise dangerous way. For example, fixing this issue
> +in Buildroot requires first for Busybox to have this fix.
> +
> +This commit imports it into Busybox and wires up the basic config. The
> +utility itself is tiny, and unlike the example code from the SeedRNG
> +project, we can re-use libbb's existing hash functions, rather than
> +having to ship a standalone BLAKE2s, which makes this even smaller.
> +
> +Cherry picked from upstream commits:
> +  - 4b407bacd4c1628782d24c3e044e43780bb057a4
> +  - 453857899616acf1c77d0d02a4c2989b2cf5f1eb
> +  - 31ec481baf106cf9c6d8f34ae6a55ab1738dea6f
> +  - 3c60711f836b151b8f361098475c3b0cd162dd17
> +  - 398bb3861aa39ae6d3ada7bb54698653b4de370b
> +  - ce9a345632786d5585044ed71ed4c98c305b918f
> +  - 3cb40f89de42aa694d44cb6e896b732fa062ee75
> +  - 57fea029cc3d6faf5a8b9ad4b17b543359fe7ccb
> +
> +Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
> +Signed-off-by: Bernhard Reutner-Fischer <rep.dot.nop@gmail.com>
> +---
> + util-linux/seedrng.c | 259 +++++++++++++++++++++++++++++++++++++++++++
> + 1 file changed, 259 insertions(+)
> + create mode 100644 util-linux/seedrng.c
> +
> +diff --git a/util-linux/seedrng.c b/util-linux/seedrng.c
> +new file mode 100644
> +index 000000000..5a41addf0
> +--- /dev/null
> ++++ b/util-linux/seedrng.c
> +@@ -0,0 +1,259 @@
> ++// SPDX-License-Identifier: GPL-2.0 OR MIT
> ++/*
> ++ * Copyright (C) 2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
> ++ *
> ++ * SeedRNG is a simple program made for seeding the Linux kernel random number
> ++ * generator from seed files. It is is useful in light of the fact that the
> ++ * Linux kernel RNG cannot be initialized from shell scripts, and new seeds
> ++ * cannot be safely generated from boot time shell scripts either. It should
> ++ * be run once at init time and once at shutdown time. It can be run at other
> ++ * times on a timer as well. Whenever it is run, it writes existing seed files
> ++ * into the RNG pool, and then creates a new seed file. If the RNG is
> ++ * initialized at the time of creating a new seed file, then that new seed file
> ++ * is marked as "creditable", which means it can be used to initialize the RNG.
> ++ * Otherwise, it is marked as "non-creditable", in which case it is still used
> ++ * to seed the RNG's pool, but will not initialize the RNG. In order to ensure
> ++ * that entropy only ever stays the same or increases from one seed file to the
> ++ * next, old seed values are hashed together with new seed values when writing
> ++ * new seed files.
> ++ *
> ++ * This is based on code from <https://git.zx2c4.com/seedrng/about/>.
> ++ */
> ++
> ++//config:config SEEDRNG
> ++//config:     bool "seedrng (2 kb)"
> ++//config:     default y
> ++//config:     help
> ++//config:     Seed the kernel RNG from seed files, meant to be called
> ++//config:     once during startup, once during shutdown, and optionally
> ++//config:     at some periodic interval in between.
> ++
> ++//applet:IF_SEEDRNG(APPLET(seedrng, BB_DIR_USR_SBIN, BB_SUID_DROP))
> ++
> ++//kbuild:lib-$(CONFIG_SEEDRNG) += seedrng.o
> ++
> ++//usage:#define seedrng_trivial_usage
> ++//usage:      "[-d SEED_DIRECTORY] [-n]"
> ++//usage:#define seedrng_full_usage "\n\n"
> ++//usage:      "Seed the kernel RNG from seed files."
> ++//usage:      "\n"
> ++//usage:      "\n     -d, --seed-dir DIR      Use seed files from specified directory (default: /var/lib/seedrng)"
> ++//usage:      "\n     -n, --skip-credit       Skip crediting seeds, even if creditable"
> ++
> ++#include "libbb.h"
> ++
> ++#include <linux/random.h>
> ++#include <sys/random.h>
> ++#include <sys/ioctl.h>
> ++#include <sys/file.h>
> ++#include <sys/stat.h>
> ++#include <sys/types.h>
> ++#include <fcntl.h>
> ++#include <poll.h>
> ++#include <unistd.h>
> ++#include <time.h>
> ++#include <errno.h>
> ++#include <endian.h>
> ++#include <stdbool.h>
> ++#include <stdint.h>
> ++#include <string.h>
> ++#include <stdio.h>
> ++#include <stdlib.h>
> ++
> ++#ifndef GRND_INSECURE
> ++#define GRND_INSECURE 0x0004 /* Apparently some headers don't ship with this yet. */
> ++#endif
> ++
> ++#define DEFAULT_SEED_DIR "/var/lib/seedrng"
> ++#define CREDITABLE_SEED_NAME "seed.credit"
> ++#define NON_CREDITABLE_SEED_NAME "seed.no-credit"
> ++
> ++enum seedrng_lengths {
> ++      MIN_SEED_LEN = SHA256_OUTSIZE,
> ++      MAX_SEED_LEN = 512
> ++};
> ++
> ++static size_t determine_optimal_seed_len(void)
> ++{
> ++      char poolsize_str[11] = { 0 };
> ++      unsigned long poolsize;
> ++
> ++      if (open_read_close("/proc/sys/kernel/random/poolsize", poolsize_str, sizeof(poolsize_str) - 1) < 0) {
> ++              bb_perror_msg("unable to determine pool size, assuming %u bits", MIN_SEED_LEN * 8);
> ++              return MIN_SEED_LEN;
> ++      }
> ++      poolsize = (bb_strtoul(poolsize_str, NULL, 10) + 7) / 8;
> ++      return MAX(MIN(poolsize, MAX_SEED_LEN), MIN_SEED_LEN);
> ++}
> ++
> ++static int read_new_seed(uint8_t *seed, size_t len, bool *is_creditable)
> ++{
> ++      ssize_t ret;
> ++
> ++      *is_creditable = false;
> ++      ret = getrandom(seed, len, GRND_NONBLOCK);
> ++      if (ret == (ssize_t)len) {
> ++              *is_creditable = true;
> ++              return 0;
> ++      } else if (ret < 0 && errno == ENOSYS) {
> ++              struct pollfd random_fd = {
> ++                      .fd = open("/dev/random", O_RDONLY),
> ++                      .events = POLLIN
> ++              };
> ++              if (random_fd.fd < 0)
> ++                      return -1;
> ++              *is_creditable = safe_poll(&random_fd, 1, 0) == 1;
> ++              close(random_fd.fd);
> ++      } else if (getrandom(seed, len, GRND_INSECURE) == (ssize_t)len)
> ++              return 0;
> ++      if (open_read_close("/dev/urandom", seed, len) == (ssize_t)len)
> ++              return 0;
> ++      return -1;
> ++}
> ++
> ++static int seed_rng(uint8_t *seed, size_t len, bool credit)
> ++{
> ++      struct {
> ++              int entropy_count;
> ++              int buf_size;
> ++              uint8_t buffer[MAX_SEED_LEN];
> ++      } req = {
> ++              .entropy_count = credit ? len * 8 : 0,
> ++              .buf_size = len
> ++      };
> ++      int random_fd, ret;
> ++
> ++      if (len > sizeof(req.buffer)) {
> ++              errno = EFBIG;
> ++              return -1;
> ++      }
> ++      memcpy(req.buffer, seed, len);
> ++
> ++      random_fd = open("/dev/urandom", O_RDONLY);
> ++      if (random_fd < 0)
> ++              return -1;
> ++      ret = ioctl(random_fd, RNDADDENTROPY, &req);
> ++      if (ret)
> ++              ret = -errno ? -errno : -EIO;
> ++      if (ENABLE_FEATURE_CLEAN_UP)
> ++              close(random_fd);
> ++      errno = -ret;
> ++      return ret ? -1 : 0;
> ++}
> ++
> ++static int seed_from_file_if_exists(const char *filename, int dfd, bool credit, sha256_ctx_t *hash)
> ++{
> ++      uint8_t seed[MAX_SEED_LEN];
> ++      ssize_t seed_len;
> ++
> ++      seed_len = open_read_close(filename, seed, sizeof(seed));
> ++      if (seed_len < 0) {
> ++              if (errno == ENOENT)
> ++                      return 0;
> ++              bb_perror_msg("unable to%s seed", " read");
> ++              return -1;
> ++      }
> ++      if ((unlink(filename) < 0 || fsync(dfd) < 0) && seed_len) {
> ++              bb_perror_msg("unable to%s seed", " remove");
> ++              return -1;
> ++      } else if (!seed_len)
> ++              return 0;
> ++
> ++      sha256_hash(hash, &seed_len, sizeof(seed_len));
> ++      sha256_hash(hash, seed, seed_len);
> ++
> ++      printf("Seeding %zd bits %s crediting\n", seed_len * 8, credit ? "and" : "without");
> ++      if (seed_rng(seed, seed_len, credit) < 0) {
> ++              bb_perror_msg("unable to%s seed", "");
> ++              return -1;
> ++      }
> ++      return 0;
> ++}
> ++
> ++int seedrng_main(int argc, char *argv[]) MAIN_EXTERNALLY_VISIBLE;
> ++int seedrng_main(int argc UNUSED_PARAM, char *argv[])
> ++{
> ++      const char *seed_dir = DEFAULT_SEED_DIR, *creditable_seed, *non_creditable_seed;
> ++      int ret, fd = -1, dfd = -1, program_ret = 0;
> ++      uint8_t new_seed[MAX_SEED_LEN];
> ++      size_t new_seed_len;
> ++      bool new_seed_creditable;
> ++      bool skip_credit = false;
> ++      struct timespec timestamp = { 0 };
> ++      sha256_ctx_t hash;
> ++
> ++      int opt;
> ++      enum {
> ++              OPT_d = (1 << 0),
> ++              OPT_n = (1 << 1)
> ++      };
> ++#if ENABLE_LONG_OPTS
> ++      static const char longopts[] ALIGN1 =
> ++              "seed-dir\0"    Required_argument       "d"
> ++              "skip-credit\0" No_argument             "n"
> ++              ;
> ++#endif
> ++
> ++      opt = getopt32long(argv, "d:n", longopts, &seed_dir);
> ++      skip_credit = opt & OPT_n;
> ++      creditable_seed = concat_path_file(seed_dir, CREDITABLE_SEED_NAME);
> ++      non_creditable_seed = concat_path_file(seed_dir, NON_CREDITABLE_SEED_NAME);
> ++
> ++      umask(0077);
> ++      if (getuid())
> ++              bb_simple_error_msg_and_die(bb_msg_you_must_be_root);
> ++
> ++      if (mkdir(seed_dir, 0700) < 0 && errno != EEXIST)
> ++              bb_perror_msg_and_die("unable to %s seed directory", "create");
> ++
> ++      dfd = open(seed_dir, O_DIRECTORY | O_RDONLY);
> ++      if (dfd < 0 || flock(dfd, LOCK_EX) < 0) {
> ++              bb_perror_msg("unable to %s seed directory", "lock");
> ++              program_ret = 1;
> ++              goto out;
> ++      }
> ++
> ++      sha256_begin(&hash);
> ++      sha256_hash(&hash, "SeedRNG v1 Old+New Prefix", 25);
> ++      clock_gettime(CLOCK_REALTIME, &timestamp);
> ++      sha256_hash(&hash, &timestamp, sizeof(timestamp));
> ++      clock_gettime(CLOCK_BOOTTIME, &timestamp);
> ++      sha256_hash(&hash, &timestamp, sizeof(timestamp));
> ++
> ++      ret = seed_from_file_if_exists(non_creditable_seed, dfd, false, &hash);
> ++      if (ret < 0)
> ++              program_ret |= 1 << 1;
> ++      ret = seed_from_file_if_exists(creditable_seed, dfd, !skip_credit, &hash);
> ++      if (ret < 0)
> ++              program_ret |= 1 << 2;
> ++
> ++      new_seed_len = determine_optimal_seed_len();
> ++      ret = read_new_seed(new_seed, new_seed_len, &new_seed_creditable);
> ++      if (ret < 0) {
> ++              bb_perror_msg("unable to%s seed", " read new");
> ++              new_seed_len = SHA256_OUTSIZE;
> ++              memset(new_seed, 0, SHA256_OUTSIZE);
> ++              program_ret |= 1 << 3;
> ++      }
> ++      sha256_hash(&hash, &new_seed_len, sizeof(new_seed_len));
> ++      sha256_hash(&hash, new_seed, new_seed_len);
> ++      sha256_end(&hash, new_seed + new_seed_len - SHA256_OUTSIZE);
> ++
> ++      printf("Saving %zu bits of %screditable seed for next boot\n", new_seed_len * 8, new_seed_creditable ? "" : "non-");
> ++      fd = open(non_creditable_seed, O_WRONLY | O_CREAT | O_TRUNC, 0400);
> ++      if (fd < 0 || full_write(fd, new_seed, new_seed_len) != (ssize_t)new_seed_len || fsync(fd) < 0) {
> ++              bb_perror_msg("unable to%s seed", " write");
> ++              program_ret |= 1 << 4;
> ++              goto out;
> ++      }
> ++      if (new_seed_creditable && rename(non_creditable_seed, creditable_seed) < 0) {
> ++              bb_simple_perror_msg("unable to make new seed creditable");
> ++              program_ret |= 1 << 5;
> ++      }
> ++out:
> ++      if (ENABLE_FEATURE_CLEAN_UP && fd >= 0)
> ++              close(fd);
> ++      if (ENABLE_FEATURE_CLEAN_UP && dfd >= 0)
> ++              close(dfd);
> ++      return program_ret;
> ++}
> +--
> +2.35.1
> +
> diff --git a/package/urandom-scripts/Config.in b/package/urandom-scripts/Config.in
> index 987e442e22..070ffa5e9a 100644
> --- a/package/urandom-scripts/Config.in
> +++ b/package/urandom-scripts/Config.in
> @@ -4,7 +4,3 @@ config BR2_PACKAGE_URANDOM_SCRIPTS
>         depends on !BR2_PACKAGE_SYSTEMD
>         help
>           Initscript to preserve the random seed between reboots.
> -
> -         WARNING: this is a poor fit to try and get high-quality
> -         entropy at boot. There are better ways, like haveged, or
> -         rng-tools.
> diff --git a/package/urandom-scripts/S20urandom b/package/urandom-scripts/S20urandom
> index 6c6aea9eee..346bc5b11e 100644
> --- a/package/urandom-scripts/S20urandom
> +++ b/package/urandom-scripts/S20urandom
> @@ -3,67 +3,31 @@
>  # Preserve the random seed between reboots. See urandom(4).
>  #
>
> -# Quietly do nothing if /dev/urandom does not exist
> -[ -c /dev/urandom ] || exit 0
> -
> -URANDOM_SEED="/var/lib/random-seed"
> +# The following knobs can be adjusted by placing uncommented modified lines
> +# into /etc/default/urandom:
> +#
> +# Set these to change where the seed and runtime lock file are stored:
> +#
> +#     seed_dir=/var/lib/seedrng
> +#
> +# Set this to true only if you do not want seed files to actually credit the
> +# RNG, for example if you plan to replicate this file system image and do not
> +# have the wherewithal to first delete the contents of /var/lib/seedrng:
> +#
> +#     skip_credit=false
> +#
>
> -# shellcheck source=/dev/null
> +unset seed_dir skip_credit
>  [ -r "/etc/default/urandom" ] && . "/etc/default/urandom"
>
> -if pool_bits=$(cat /proc/sys/kernel/random/poolsize 2> /dev/null); then
> -       pool_size=$((pool_bits/8))
> -else
> -       pool_size=512
> -fi
> -
> -init_rng() {
> -       [ -f "$URANDOM_SEED" ] || return 0
> -       printf 'Initializing random number generator: '
> -       dd if="$URANDOM_SEED" bs="$pool_size" of=/dev/urandom count=1 2> /dev/null
> -       status=$?
> -       if [ "$status" -eq 0 ]; then
> -               echo "OK"
> -       else
> -               echo "FAIL"
> -       fi
> -       return "$status"
> -}
> -
> -save_random_seed() {
> -       printf 'Saving random seed: '
> -       status=1
> -       if touch "$URANDOM_SEED.new" 2> /dev/null; then
> -               old_umask=$(umask)
> -               umask 077
> -               dd if=/dev/urandom of="$URANDOM_SEED.tmp" bs="$pool_size" count=1 2> /dev/null
> -               cat "$URANDOM_SEED" "$URANDOM_SEED.tmp" 2>/dev/null \
> -                       | sha256sum \
> -                       | cut -d ' ' -f 1 > "$URANDOM_SEED.new" && \
> -               mv "$URANDOM_SEED.new" "$URANDOM_SEED" && status=0
> -               rm -f "$URANDOM_SEED.tmp"
> -               umask "$old_umask"
> -               if [ "$status" -eq 0 ]; then
> -                       echo "OK"
> -               else
> -                       echo "FAIL"
> -               fi
> -
> -       else
> -               echo "SKIP (read-only file system detected)"
> -       fi
> -       return "$status"
> -}
> -
>  case "$1" in
> -       start|restart|reload)
> -               # Carry a random seed from start-up to start-up
> -               # Load and then save the whole entropy pool
> -               init_rng && save_random_seed;;
> -       stop)
> -               # Carry a random seed from shut-down to start-up
> -               # Save the whole entropy pool
> -               save_random_seed;;
> +       start|stop|restart|reload)
> +               # Never fail, as this isn't worth making a fuss
> +               # over if it doesn't go as planned.
> +               args=""
> +               [ -n "$seed_dir" ] && args="$args --seed-dir=$seed_dir"
> +               [ "$skip_credit" = "true" ] && args="$args --skip-credit"
> +               seedrng $args || true;;
>         *)
>                 echo "Usage: $0 {start|stop|restart|reload}"
>                 exit 1
> --
> 2.35.1
>
> _______________________________________________
> buildroot mailing list
> buildroot@buildroot.org
> https://lists.buildroot.org/mailman/listinfo/buildroot
Arnout Vandecappelle Feb. 7, 2023, 3:06 p.m. UTC | #4
On 20/04/2022 19:03, Jason A. Donenfeld wrote:
> The RNG can't actually be seeded from a shell script, due to the
> reliance on ioctls. For this reason, Busybox now contains SeedRNG, a
> tiny program meant to be called at startup and shutdown (and at
> arbitrary other points in between if desired).
> 
> Since this is currently in Busybox's git tree, this commit adds it as a
> patch to the Busybox package. Then, it wires the init script up to call
> it.
> 
> This is a significant improvement over the current init script, which
> doesn't credit entropy and whose hashing in shell scripts is sort of
> fragile.
> 
> NOTE NOTE NOTE: While I've added the patch to the Busybox package
> directory, I don't really know how to wire this up and what to do about
> the various config entries. I was hoping that a Buildroot maintainer
> might have a better idea of the best way to do this, and could just take
> this patch as a starting point for whatever the real commit winds up
> being.
> 
> Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>

  Applied to master, after some reworking and heavily rewriting the commit 
message, thanks.

     package/urandom-scripts: actually credit seed files via seedrng

     The RNG can't actually be seeded from a shell script, due to the
     reliance on ioctls. For this reason, Busybox 1.36.0 contains SeedRNG, a
     tiny program meant to be called at startup and shutdown (and at
     arbitrary other points in between if desired). Note that initially,
     the way seedrng was included in busybox broke things quite severely, but
     now it's been reverted to a reasonably acceptable version.

     This is a significant improvement over the current init script, which
     doesn't credit entropy and whose hashing in shell scripts is sort of
     fragile.

     Because seedrng is part of busybox, urandom-scripts now depends on
     BR2_PACKAGE_BUSYBOX. This can be removed again if later we add a
     standalone seedrng package.

     Add a decent explanation to the init script about the need for a
     persistent directory to make this actually work.

     Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
     [Arnout:
      - Remove busybox patch, it's now part of busybox 1.36.0.
      - Depend on busybox.
      - Fix shellcheck errors.
      - Use DAEMON and SEEDRNG_ARGS.
      - Don't bother with "seed_dir" and "skip_credit" variables.
      - Rename to S20seedrng.
     ]
     Signed-off-by: Arnout Vandecappelle <arnout@mind.be>


  Regards,
  Arnout

> ---
> Changes v4->v5:
> - SeedRNG is now part of Busybox, so we don't need to carry the source
>    in-tree here, which was the primary objection to earlier versions.
> 
>   ...eedRNG-utility-for-kernel-RNG-seed-f.patch | 309 ++++++++++++++++++
>   package/urandom-scripts/Config.in             |   4 -
>   package/urandom-scripts/S20urandom            |  78 ++---
>   3 files changed, 330 insertions(+), 61 deletions(-)
>   create mode 100644 package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch
> 
[snip]
Jason A. Donenfeld Feb. 7, 2023, 5:23 p.m. UTC | #5
On Tue, Feb 07, 2023 at 04:06:16PM +0100, Arnout Vandecappelle wrote:
> 
> 
> On 20/04/2022 19:03, Jason A. Donenfeld wrote:
> > The RNG can't actually be seeded from a shell script, due to the
> > reliance on ioctls. For this reason, Busybox now contains SeedRNG, a
> > tiny program meant to be called at startup and shutdown (and at
> > arbitrary other points in between if desired).
> > 
> > Since this is currently in Busybox's git tree, this commit adds it as a
> > patch to the Busybox package. Then, it wires the init script up to call
> > it.
> > 
> > This is a significant improvement over the current init script, which
> > doesn't credit entropy and whose hashing in shell scripts is sort of
> > fragile.
> > 
> > NOTE NOTE NOTE: While I've added the patch to the Busybox package
> > directory, I don't really know how to wire this up and what to do about
> > the various config entries. I was hoping that a Buildroot maintainer
> > might have a better idea of the best way to do this, and could just take
> > this patch as a starting point for whatever the real commit winds up
> > being.
> > 
> > Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
> 
>   Applied to master, after some reworking and heavily rewriting the commit 
> message, thanks.
> 
>      package/urandom-scripts: actually credit seed files via seedrng
> 
>      The RNG can't actually be seeded from a shell script, due to the
>      reliance on ioctls. For this reason, Busybox 1.36.0 contains SeedRNG, a
>      tiny program meant to be called at startup and shutdown (and at
>      arbitrary other points in between if desired). Note that initially,
>      the way seedrng was included in busybox broke things quite severely, but
>      now it's been reverted to a reasonably acceptable version.
> 
>      This is a significant improvement over the current init script, which
>      doesn't credit entropy and whose hashing in shell scripts is sort of
>      fragile.
> 
>      Because seedrng is part of busybox, urandom-scripts now depends on
>      BR2_PACKAGE_BUSYBOX. This can be removed again if later we add a
>      standalone seedrng package.
> 
>      Add a decent explanation to the init script about the need for a
>      persistent directory to make this actually work.
> 
>      Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
>      [Arnout:
>       - Remove busybox patch, it's now part of busybox 1.36.0.
>       - Depend on busybox.
>       - Fix shellcheck errors.
>       - Use DAEMON and SEEDRNG_ARGS.
>       - Don't bother with "seed_dir" and "skip_credit" variables.
>       - Rename to S20seedrng.
>      ]
>      Signed-off-by: Arnout Vandecappelle <arnout@mind.be>

Looks good. Thank you very much for following up and bringing this
across the finish line!

Jason
diff mbox series

Patch

diff --git a/package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch b/package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch
new file mode 100644
index 0000000000..493c31b247
--- /dev/null
+++ b/package/busybox/0003-seedrng-import-SeedRNG-utility-for-kernel-RNG-seed-f.patch
@@ -0,0 +1,309 @@ 
+From 47660dc9581e251f555ca9b05b630276f1d487ee Mon Sep 17 00:00:00 2001
+From: "Jason A. Donenfeld" <Jason@zx2c4.com>
+Date: Mon, 4 Apr 2022 18:21:51 +0200
+Subject: [PATCH] seedrng: import SeedRNG utility for kernel RNG seed files
+
+The RNG can't actually be seeded from a shell script, due to the
+reliance on ioctls and the fact that entropy written into the
+unprivileged /dev/urandom device is not immediately mixed in, making
+subsequent seed reads dangerous. For this reason, the seedrng project
+provides a basic "C script" meant to be copy and pasted into projects
+like Busybox and tweaked as needed: <https://git.zx2c4.com/seedrng/about/>.
+
+The SeedRNG construction has been part of systemd's seeder since
+January, and recently was added to Android, OpenRC, and Void's Runit,
+with more integrations on their way depending on context. Virtually
+every single Busybox-based distro I have seen seeds things in wrong,
+incomplete, or otherwise dangerous way. For example, fixing this issue
+in Buildroot requires first for Busybox to have this fix.
+
+This commit imports it into Busybox and wires up the basic config. The
+utility itself is tiny, and unlike the example code from the SeedRNG
+project, we can re-use libbb's existing hash functions, rather than
+having to ship a standalone BLAKE2s, which makes this even smaller.
+
+Cherry picked from upstream commits:
+  - 4b407bacd4c1628782d24c3e044e43780bb057a4
+  - 453857899616acf1c77d0d02a4c2989b2cf5f1eb
+  - 31ec481baf106cf9c6d8f34ae6a55ab1738dea6f
+  - 3c60711f836b151b8f361098475c3b0cd162dd17
+  - 398bb3861aa39ae6d3ada7bb54698653b4de370b
+  - ce9a345632786d5585044ed71ed4c98c305b918f
+  - 3cb40f89de42aa694d44cb6e896b732fa062ee75
+  - 57fea029cc3d6faf5a8b9ad4b17b543359fe7ccb
+
+Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
+Signed-off-by: Bernhard Reutner-Fischer <rep.dot.nop@gmail.com>
+---
+ util-linux/seedrng.c | 259 +++++++++++++++++++++++++++++++++++++++++++
+ 1 file changed, 259 insertions(+)
+ create mode 100644 util-linux/seedrng.c
+
+diff --git a/util-linux/seedrng.c b/util-linux/seedrng.c
+new file mode 100644
+index 000000000..5a41addf0
+--- /dev/null
++++ b/util-linux/seedrng.c
+@@ -0,0 +1,259 @@
++// SPDX-License-Identifier: GPL-2.0 OR MIT
++/*
++ * Copyright (C) 2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
++ *
++ * SeedRNG is a simple program made for seeding the Linux kernel random number
++ * generator from seed files. It is is useful in light of the fact that the
++ * Linux kernel RNG cannot be initialized from shell scripts, and new seeds
++ * cannot be safely generated from boot time shell scripts either. It should
++ * be run once at init time and once at shutdown time. It can be run at other
++ * times on a timer as well. Whenever it is run, it writes existing seed files
++ * into the RNG pool, and then creates a new seed file. If the RNG is
++ * initialized at the time of creating a new seed file, then that new seed file
++ * is marked as "creditable", which means it can be used to initialize the RNG.
++ * Otherwise, it is marked as "non-creditable", in which case it is still used
++ * to seed the RNG's pool, but will not initialize the RNG. In order to ensure
++ * that entropy only ever stays the same or increases from one seed file to the
++ * next, old seed values are hashed together with new seed values when writing
++ * new seed files.
++ *
++ * This is based on code from <https://git.zx2c4.com/seedrng/about/>.
++ */
++
++//config:config SEEDRNG
++//config:	bool "seedrng (2 kb)"
++//config:	default y
++//config:	help
++//config:	Seed the kernel RNG from seed files, meant to be called
++//config:	once during startup, once during shutdown, and optionally
++//config:	at some periodic interval in between.
++
++//applet:IF_SEEDRNG(APPLET(seedrng, BB_DIR_USR_SBIN, BB_SUID_DROP))
++
++//kbuild:lib-$(CONFIG_SEEDRNG) += seedrng.o
++
++//usage:#define seedrng_trivial_usage
++//usage:	"[-d SEED_DIRECTORY] [-n]"
++//usage:#define seedrng_full_usage "\n\n"
++//usage:	"Seed the kernel RNG from seed files."
++//usage:	"\n"
++//usage:	"\n	-d, --seed-dir DIR	Use seed files from specified directory (default: /var/lib/seedrng)"
++//usage:	"\n	-n, --skip-credit	Skip crediting seeds, even if creditable"
++
++#include "libbb.h"
++
++#include <linux/random.h>
++#include <sys/random.h>
++#include <sys/ioctl.h>
++#include <sys/file.h>
++#include <sys/stat.h>
++#include <sys/types.h>
++#include <fcntl.h>
++#include <poll.h>
++#include <unistd.h>
++#include <time.h>
++#include <errno.h>
++#include <endian.h>
++#include <stdbool.h>
++#include <stdint.h>
++#include <string.h>
++#include <stdio.h>
++#include <stdlib.h>
++
++#ifndef GRND_INSECURE
++#define GRND_INSECURE 0x0004 /* Apparently some headers don't ship with this yet. */
++#endif
++
++#define DEFAULT_SEED_DIR "/var/lib/seedrng"
++#define CREDITABLE_SEED_NAME "seed.credit"
++#define NON_CREDITABLE_SEED_NAME "seed.no-credit"
++
++enum seedrng_lengths {
++	MIN_SEED_LEN = SHA256_OUTSIZE,
++	MAX_SEED_LEN = 512
++};
++
++static size_t determine_optimal_seed_len(void)
++{
++	char poolsize_str[11] = { 0 };
++	unsigned long poolsize;
++
++	if (open_read_close("/proc/sys/kernel/random/poolsize", poolsize_str, sizeof(poolsize_str) - 1) < 0) {
++		bb_perror_msg("unable to determine pool size, assuming %u bits", MIN_SEED_LEN * 8);
++		return MIN_SEED_LEN;
++	}
++	poolsize = (bb_strtoul(poolsize_str, NULL, 10) + 7) / 8;
++	return MAX(MIN(poolsize, MAX_SEED_LEN), MIN_SEED_LEN);
++}
++
++static int read_new_seed(uint8_t *seed, size_t len, bool *is_creditable)
++{
++	ssize_t ret;
++
++	*is_creditable = false;
++	ret = getrandom(seed, len, GRND_NONBLOCK);
++	if (ret == (ssize_t)len) {
++		*is_creditable = true;
++		return 0;
++	} else if (ret < 0 && errno == ENOSYS) {
++		struct pollfd random_fd = {
++			.fd = open("/dev/random", O_RDONLY),
++			.events = POLLIN
++		};
++		if (random_fd.fd < 0)
++			return -1;
++		*is_creditable = safe_poll(&random_fd, 1, 0) == 1;
++		close(random_fd.fd);
++	} else if (getrandom(seed, len, GRND_INSECURE) == (ssize_t)len)
++		return 0;
++	if (open_read_close("/dev/urandom", seed, len) == (ssize_t)len)
++		return 0;
++	return -1;
++}
++
++static int seed_rng(uint8_t *seed, size_t len, bool credit)
++{
++	struct {
++		int entropy_count;
++		int buf_size;
++		uint8_t buffer[MAX_SEED_LEN];
++	} req = {
++		.entropy_count = credit ? len * 8 : 0,
++		.buf_size = len
++	};
++	int random_fd, ret;
++
++	if (len > sizeof(req.buffer)) {
++		errno = EFBIG;
++		return -1;
++	}
++	memcpy(req.buffer, seed, len);
++
++	random_fd = open("/dev/urandom", O_RDONLY);
++	if (random_fd < 0)
++		return -1;
++	ret = ioctl(random_fd, RNDADDENTROPY, &req);
++	if (ret)
++		ret = -errno ? -errno : -EIO;
++	if (ENABLE_FEATURE_CLEAN_UP)
++		close(random_fd);
++	errno = -ret;
++	return ret ? -1 : 0;
++}
++
++static int seed_from_file_if_exists(const char *filename, int dfd, bool credit, sha256_ctx_t *hash)
++{
++	uint8_t seed[MAX_SEED_LEN];
++	ssize_t seed_len;
++
++	seed_len = open_read_close(filename, seed, sizeof(seed));
++	if (seed_len < 0) {
++		if (errno == ENOENT)
++			return 0;
++		bb_perror_msg("unable to%s seed", " read");
++		return -1;
++	}
++	if ((unlink(filename) < 0 || fsync(dfd) < 0) && seed_len) {
++		bb_perror_msg("unable to%s seed", " remove");
++		return -1;
++	} else if (!seed_len)
++		return 0;
++
++	sha256_hash(hash, &seed_len, sizeof(seed_len));
++	sha256_hash(hash, seed, seed_len);
++
++	printf("Seeding %zd bits %s crediting\n", seed_len * 8, credit ? "and" : "without");
++	if (seed_rng(seed, seed_len, credit) < 0) {
++		bb_perror_msg("unable to%s seed", "");
++		return -1;
++	}
++	return 0;
++}
++
++int seedrng_main(int argc, char *argv[]) MAIN_EXTERNALLY_VISIBLE;
++int seedrng_main(int argc UNUSED_PARAM, char *argv[])
++{
++	const char *seed_dir = DEFAULT_SEED_DIR, *creditable_seed, *non_creditable_seed;
++	int ret, fd = -1, dfd = -1, program_ret = 0;
++	uint8_t new_seed[MAX_SEED_LEN];
++	size_t new_seed_len;
++	bool new_seed_creditable;
++	bool skip_credit = false;
++	struct timespec timestamp = { 0 };
++	sha256_ctx_t hash;
++
++	int opt;
++	enum {
++		OPT_d = (1 << 0),
++		OPT_n = (1 << 1)
++	};
++#if ENABLE_LONG_OPTS
++	static const char longopts[] ALIGN1 =
++		"seed-dir\0"	Required_argument	"d"
++		"skip-credit\0"	No_argument		"n"
++		;
++#endif
++
++	opt = getopt32long(argv, "d:n", longopts, &seed_dir);
++	skip_credit = opt & OPT_n;
++	creditable_seed = concat_path_file(seed_dir, CREDITABLE_SEED_NAME);
++	non_creditable_seed = concat_path_file(seed_dir, NON_CREDITABLE_SEED_NAME);
++
++	umask(0077);
++	if (getuid())
++		bb_simple_error_msg_and_die(bb_msg_you_must_be_root);
++
++	if (mkdir(seed_dir, 0700) < 0 && errno != EEXIST)
++		bb_perror_msg_and_die("unable to %s seed directory", "create");
++
++	dfd = open(seed_dir, O_DIRECTORY | O_RDONLY);
++	if (dfd < 0 || flock(dfd, LOCK_EX) < 0) {
++		bb_perror_msg("unable to %s seed directory", "lock");
++		program_ret = 1;
++		goto out;
++	}
++
++	sha256_begin(&hash);
++	sha256_hash(&hash, "SeedRNG v1 Old+New Prefix", 25);
++	clock_gettime(CLOCK_REALTIME, &timestamp);
++	sha256_hash(&hash, &timestamp, sizeof(timestamp));
++	clock_gettime(CLOCK_BOOTTIME, &timestamp);
++	sha256_hash(&hash, &timestamp, sizeof(timestamp));
++
++	ret = seed_from_file_if_exists(non_creditable_seed, dfd, false, &hash);
++	if (ret < 0)
++		program_ret |= 1 << 1;
++	ret = seed_from_file_if_exists(creditable_seed, dfd, !skip_credit, &hash);
++	if (ret < 0)
++		program_ret |= 1 << 2;
++
++	new_seed_len = determine_optimal_seed_len();
++	ret = read_new_seed(new_seed, new_seed_len, &new_seed_creditable);
++	if (ret < 0) {
++		bb_perror_msg("unable to%s seed", " read new");
++		new_seed_len = SHA256_OUTSIZE;
++		memset(new_seed, 0, SHA256_OUTSIZE);
++		program_ret |= 1 << 3;
++	}
++	sha256_hash(&hash, &new_seed_len, sizeof(new_seed_len));
++	sha256_hash(&hash, new_seed, new_seed_len);
++	sha256_end(&hash, new_seed + new_seed_len - SHA256_OUTSIZE);
++
++	printf("Saving %zu bits of %screditable seed for next boot\n", new_seed_len * 8, new_seed_creditable ? "" : "non-");
++	fd = open(non_creditable_seed, O_WRONLY | O_CREAT | O_TRUNC, 0400);
++	if (fd < 0 || full_write(fd, new_seed, new_seed_len) != (ssize_t)new_seed_len || fsync(fd) < 0) {
++		bb_perror_msg("unable to%s seed", " write");
++		program_ret |= 1 << 4;
++		goto out;
++	}
++	if (new_seed_creditable && rename(non_creditable_seed, creditable_seed) < 0) {
++		bb_simple_perror_msg("unable to make new seed creditable");
++		program_ret |= 1 << 5;
++	}
++out:
++	if (ENABLE_FEATURE_CLEAN_UP && fd >= 0)
++		close(fd);
++	if (ENABLE_FEATURE_CLEAN_UP && dfd >= 0)
++		close(dfd);
++	return program_ret;
++}
+-- 
+2.35.1
+
diff --git a/package/urandom-scripts/Config.in b/package/urandom-scripts/Config.in
index 987e442e22..070ffa5e9a 100644
--- a/package/urandom-scripts/Config.in
+++ b/package/urandom-scripts/Config.in
@@ -4,7 +4,3 @@  config BR2_PACKAGE_URANDOM_SCRIPTS
 	depends on !BR2_PACKAGE_SYSTEMD
 	help
 	  Initscript to preserve the random seed between reboots.
-
-	  WARNING: this is a poor fit to try and get high-quality
-	  entropy at boot. There are better ways, like haveged, or
-	  rng-tools.
diff --git a/package/urandom-scripts/S20urandom b/package/urandom-scripts/S20urandom
index 6c6aea9eee..346bc5b11e 100644
--- a/package/urandom-scripts/S20urandom
+++ b/package/urandom-scripts/S20urandom
@@ -3,67 +3,31 @@ 
 # Preserve the random seed between reboots. See urandom(4).
 #
 
-# Quietly do nothing if /dev/urandom does not exist
-[ -c /dev/urandom ] || exit 0
-
-URANDOM_SEED="/var/lib/random-seed"
+# The following knobs can be adjusted by placing uncommented modified lines
+# into /etc/default/urandom:
+#
+# Set these to change where the seed and runtime lock file are stored:
+#
+#     seed_dir=/var/lib/seedrng
+#
+# Set this to true only if you do not want seed files to actually credit the
+# RNG, for example if you plan to replicate this file system image and do not
+# have the wherewithal to first delete the contents of /var/lib/seedrng:
+#
+#     skip_credit=false
+#
 
-# shellcheck source=/dev/null
+unset seed_dir skip_credit
 [ -r "/etc/default/urandom" ] && . "/etc/default/urandom"
 
-if pool_bits=$(cat /proc/sys/kernel/random/poolsize 2> /dev/null); then
-	pool_size=$((pool_bits/8))
-else
-	pool_size=512
-fi
-
-init_rng() {
-	[ -f "$URANDOM_SEED" ] || return 0
-	printf 'Initializing random number generator: '
-	dd if="$URANDOM_SEED" bs="$pool_size" of=/dev/urandom count=1 2> /dev/null
-	status=$?
-	if [ "$status" -eq 0 ]; then
-		echo "OK"
-	else
-		echo "FAIL"
-	fi
-	return "$status"
-}
-
-save_random_seed() {
-	printf 'Saving random seed: '
-	status=1
-	if touch "$URANDOM_SEED.new" 2> /dev/null; then
-		old_umask=$(umask)
-		umask 077
-		dd if=/dev/urandom of="$URANDOM_SEED.tmp" bs="$pool_size" count=1 2> /dev/null
-		cat "$URANDOM_SEED" "$URANDOM_SEED.tmp" 2>/dev/null \
-			| sha256sum \
-			| cut -d ' ' -f 1 > "$URANDOM_SEED.new" && \
-		mv "$URANDOM_SEED.new" "$URANDOM_SEED" && status=0
-		rm -f "$URANDOM_SEED.tmp"
-		umask "$old_umask"
-		if [ "$status" -eq 0 ]; then
-			echo "OK"
-		else
-			echo "FAIL"
-		fi
-
-	else
-		echo "SKIP (read-only file system detected)"
-	fi
-	return "$status"
-}
-
 case "$1" in
-	start|restart|reload)
-		# Carry a random seed from start-up to start-up
-		# Load and then save the whole entropy pool
-		init_rng && save_random_seed;;
-	stop)
-		# Carry a random seed from shut-down to start-up
-		# Save the whole entropy pool
-		save_random_seed;;
+	start|stop|restart|reload)
+		# Never fail, as this isn't worth making a fuss
+		# over if it doesn't go as planned.
+		args=""
+		[ -n "$seed_dir" ] && args="$args --seed-dir=$seed_dir"
+		[ "$skip_credit" = "true" ] && args="$args --skip-credit"
+		seedrng $args || true;;
 	*)
 		echo "Usage: $0 {start|stop|restart|reload}"
 		exit 1