ordnance: Initialize Linux Random Number Generator (RNG) early

We start using Linux RNG from initrd with low entropy pools and random
data quality might not be good. Kernel warns us about the problem with
following messages in dmesg(1):

  [    4.786307] random: onl-mounts: uninitialized urandom read (16 bytes read, 46 bits of entropy available)
  [    5.307536] random: onl-mounts: uninitialized urandom read (16 bytes read, 83 bits of entropy available)
  [    5.354480] random: blkid: uninitialized urandom read (6 bytes read, 89 bits of entropy available)
  [    5.366963] random: blkid: uninitialized urandom read (6 bytes read, 90 bits of entropy available)
  [    5.379385] random: blkid: uninitialized urandom read (6 bytes read, 90 bits of entropy available)
  [    5.391910] random: blkid: uninitialized urandom read (6 bytes read, 90 bits of entropy available)
  [    5.546389] random: onl-pki: uninitialized urandom read (16 bytes read, 96 bits of entropy available)
  [    8.881398] random: mktemp: uninitialized urandom read (6 bytes read, 109 bits of entropy available)
  [    9.026771] random: swiget: uninitialized urandom read (16 bytes read, 109 bits of entropy available)

Since main rootfs isn't mounted we can't load entropy saved from
previous runtime by systemd-random-seed (for systemd) and
/etc/init.d/urandom (for sysvinit).

Moreover even if we able to load this data, direct write to /dev/urandom
or /dev/random does not change entropy count according to random(4) man
page and /proc/sys/kernel/random/entropy_avail contents after loading
data to /dev/urandom or /dev/random.

To address this we should generate pseudo random data suitable for use
as RNG seed based on frequently changed information in system and use
some cryptographic grade hash to hide this info from RNG.

Use MIT licensed initrng.py Python implementation for Linux RNG early
init to seed RNG before executing onl-mounts and other stuff from early
userspace in initramfs.

Signed-off-by: Sergey Popovich <sergey.popovich@ordnance.co>
This commit is contained in:
Sergey Popovich
2018-08-16 19:25:41 +03:00
parent a5ec39317e
commit e12e40f335
2 changed files with 233 additions and 1 deletions

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python
"""
Distributed under MIT License.
See LICENSE file for full license text.
"""
import fcntl
import struct
import sys
import os
import hashlib
import logging
import argparse
RNDADDENTROPY = 0x40085203 # from linux/random.h
def sha512sum(digest, fileName, blockSize = 16 * 1024):
"""
Take SHA512 digest from file given by it's name contents.
File opened in os.O_NONBLOCK mode to support FIFOs and
special files like /dev/kmsg. Read data from files in
blockSize (default 16kB) chunks to keep memory usage at
minimum when used with large files.
Return True on success and False otherwise.
"""
def read():
try:
return fp.read(blockSize)
except IOError:
return b''
#end def
if digest is None:
return False
try:
fp = open(fileName, 'rb')
except IOError:
return False
flags = fcntl.fcntl(fp, fcntl.F_GETFL) | os.O_NONBLOCK
if fcntl.fcntl(fp, fcntl.F_SETFL, flags):
fp.close()
return False
for block in iter(read, b''):
digest.update(block)
fp.close()
return True
def sha512(digest, fileName):
"""
Computes sha512sum() optionally logging status.
For internal use only.
Return 1 on success and 0 on failure.
"""
rc = sha512sum(digest, fileName)
logging.debug("SHA512 (%s) = %s", fileName, 'ok' if rc else 'fail')
return int(rc)
def add_entropy(digest, fileName = "/dev/urandom"):
"""
Adds digest.digest_size * 8 bits of entropy using ioctl(RNDADDENTROPY, ...)
to increase entropy count if fileName is a character special file node and
CAP_SYS_ADMIN is available (otherwise IOError is raised by fnctl.fnctl()).
Writes entropy to fileName if IOError exception is raised by fnctl.fnctl()
or fileName is a regular file. Update entropy pool without incrementing
entropy count if fileName is special character device like "/dev/urandom".
"""
try:
fp = open(fileName, 'ab')
except IOError as e:
return None, str(e)
size = digest.digest_size
digest = digest.digest()
method = None
err = None
do_ioctl = not os.path.isfile(fileName)
try:
if do_ioctl:
# from random(4):
#
# struct rand_pool_info {
# int entropy_count;
# int buf_size;
# __u32 buf[0];
# };
#
fmt = "ii{:d}s".format(size)
rand_pool_info = struct.pack(fmt, size * 8, size, digest)
fcntl.ioctl(fp, RNDADDENTROPY, rand_pool_info)
else:
raise IOError()
except IOError:
try:
fp.write(digest)
except IOError as e:
err = str(e)
else:
method = "write"
else:
method = "ioctl"
return method, err
def init():
prog_name = os.path.splitext(os.path.basename(__file__))[0]
if not prog_name:
prog_name = "initrng"
parser = argparse.ArgumentParser(description = 'Linux RNG early init')
# loglevel
loglevels = {
'crit' : 'CRITICAL',
'err' : 'ERROR',
'warn' : 'WARNING',
'info' : 'INFO',
'debug': 'DEBUG',
}
parser.add_argument('-l', '--loglevel', default = 'info',
choices = list(loglevels.keys()),
help = 'set program loging severity (level)')
# entropy_files
dflt_entropy_files = [
"/proc/timer_list",
"/proc/buddyinfo",
"/proc/interrupts",
"/proc/softirqs",
]
sched_debug = "/proc/sched_debug"
if os.path.isfile(sched_debug):
dflt_entropy_files.append(sched_debug)
else:
dflt_entropy_files.append("/proc/schedstat")
parser.add_argument('-e', '--entropy-file', default = [],
action = 'append', dest = 'entropy_files', type = str,
help = 'files to use as source of entropy ({:s})'.format(', '.join(dflt_entropy_files)))
# repeat
dflt_repeat = 8
parser.add_argument('-r', '--repeat', default = dflt_repeat,
action = 'store', type = int,
help = 'repeat entropy updates # times (default {:d})'.format(dflt_repeat))
# output
dflt_output = "/dev/urandom"
parser.add_argument('-o', '--output', default = dflt_output,
action = 'store', type = str,
help = 'file to output entropy (default "{:s}")'.format(dflt_output))
args = parser.parse_args()
logging.basicConfig(format = "{:s}: %(message)s".format(prog_name),
level = getattr(logging, loglevels[args.loglevel]))
# adjust entropy_files
if not args.entropy_files:
args.entropy_files = dflt_entropy_files
logging.debug("using default list of files as entropy source")
# adjust repeat
repeat = args.repeat
if repeat <= 0:
repeat = 1
elif repeat > 65536:
repeat = 65536
if repeat != args.repeat:
logging.debug("adjust repeat count from %d to %d", args.repeat, repeat)
args.repeat = repeat
# truncate entropy file
output = args.output
try:
with open(output, 'wb'):
pass
except IOError:
logging.debug("entropy file '%s' isn't writable", output)
args = None
else:
logging.debug("entropy file '%s' is writable", output)
return args
if __name__ == '__main__':
args = init()
if args is None:
logging.debug("fail to adjust/validate args")
sys.exit(1)
logging.info("Linux Random Number Generator (RNG) early init")
entropy_files = args.entropy_files
repeat = args.repeat
for x in range(1, repeat + 1):
digest_sha512 = hashlib.sha512()
i = 0
for f in entropy_files:
i += sha512(digest_sha512, f)
if not i:
logging.debug("digest error for all entropy files, step %d", x)
continue
method, err = add_entropy(digest_sha512, args.output)
if err:
logging.debug("error seeding Linux RNG, step %d: %s", x, err)
else:
logging.debug("seeded Linux RNG, method '%s', step %d", method, x)

View File

@@ -47,8 +47,10 @@ case "$(stat -f -c "%T" /tmp)" in
;;
esac
# Grab cmdline settings
# Initialize RNG early
python /bin/initrng.py
# Grab cmdline settings
touch /etc/onl/boot-config
tr -s " " "\n" </proc/cmdline |
while read -r l; do