diff options
Diffstat (limited to 'opt/tinyrd')
| -rw-r--r-- | opt/tinyrd/.footprint | 2 | ||||
| -rw-r--r-- | opt/tinyrd/.signature | 8 | ||||
| -rw-r--r-- | opt/tinyrd/MAKEPKG | 75 | ||||
| -rw-r--r-- | opt/tinyrd/tinyrd.c | 526 |
4 files changed, 611 insertions, 0 deletions
diff --git a/opt/tinyrd/.footprint b/opt/tinyrd/.footprint new file mode 100644 index 0000000..eff12bd --- /dev/null +++ b/opt/tinyrd/.footprint @@ -0,0 +1,2 @@ +drwxr-xr-x root/root boot/ +-rw-r--r-- root/root boot/tinyrd.img diff --git a/opt/tinyrd/.signature b/opt/tinyrd/.signature new file mode 100644 index 0000000..97fbf65 --- /dev/null +++ b/opt/tinyrd/.signature @@ -0,0 +1,8 @@ +RWTZ9IduCSQ/mJOxIZ0dTcfhHbV+FMPe2veVvxydOeP758894pIqTG7nqTzqpOk/EQCdpZFDaiqljWeMr+0q0YiGnXn4op8KTQo= + +SHA256 (MAKEPKG) = 71de855fde5c2f4afe668f6adfb56e80feebeba63546a26d983a7dc8f982bd7a +SHA256 (.footprint) = 1ea2580bfeb3deb69693fdbd86ba941e9f49a8e6de4aabde25480471cec53f3f +SHA256 (tinyrd.c) = 79dc8126afab289ff924f382face04ad18ae53d3257a7349dbef52d864c6b590 +SHA256 (cryptsetup-2.8.8.tar.xz) = 3acfa685f2dd7fcc832e0b77bc7093aa7da554a51ce8dafbb4138eaa854eee35 +SHA256 (LVM2.2.03.35.tgz) = ebf28b3427535e2b5abd9991cd839b61622a0dbfb8c86df0f7af1f69dcaa8371 +SHA256 (tinyrd#0.1-2.pkg.tar.gz) = ca2af533897984e79fd44e5467d9c57248f9adf4e114d81d0fa8e5c90dfeecff diff --git a/opt/tinyrd/MAKEPKG b/opt/tinyrd/MAKEPKG new file mode 100644 index 0000000..a8cce18 --- /dev/null +++ b/opt/tinyrd/MAKEPKG @@ -0,0 +1,75 @@ +#!/bin/mkpkg +# description: rawnix initramfs: static /init that unlocks a LUKS root +# url: https://rawnix.org +# +# The image is generic (no modules, no config), so one package serves every +# kernel and nothing regenerates it. Staged in /boot; limine-sync mirrors it. +# +# libcryptsetup and libdevmapper are built here as private static libraries +# and never installed. The system ports stay shared-only, and the initramfs +# wants a different configuration anyway: +# - libdevmapper without udev_sync (no udev before root; the system one +# has it, and would drag in libudev.a) +# - kernel crypto backend (no libcrypto.a; needs CRYPTO_USER_API_HASH=y) +# - internal argon2, no blkid, no tokens +# +# Versions are literal below (source= and the two cd lines): keep cryptsetup +# equal to the cryptsetup port and LVM2 equal to device-mapper. A security fix in either reaches the initramfs only by bumping release here. + +name=tinyrd +version=0.1 +release=2 +depends=() +makedeps=(pkgconf popt util-linux json-c) +source=(tinyrd.c + https://www.kernel.org/pub/linux/utils/cryptsetup/v2.8/cryptsetup-2.8.8.tar.xz + https://sourceware.org/pub/lvm2/LVM2.2.03.35.tgz) + +sha256sums=( + "79dc8126afab289ff924f382face04ad18ae53d3257a7349dbef52d864c6b590" + "3acfa685f2dd7fcc832e0b77bc7093aa7da554a51ce8dafbb4138eaa854eee35" + "ebf28b3427535e2b5abd9991cd839b61622a0dbfb8c86df0f7af1f69dcaa8371" +) + +build() { + top=$PWD + _p=$top/static # private prefix, never packaged + export CC=clang + export PKG_CONFIG_PATH=$_p/lib/pkgconfig:$PKG_CONFIG_PATH + + cd $top/LVM2.2.03.35 + ./configure --prefix=$_p --enable-static_link --disable-udev_sync \ + --disable-selinux --disable-blkid_wiping --disable-readline --disable-nls \ + --with-thin=none --with-cache=none --with-vdo=none \ + --with-writecache=none --with-integrity=none + make -C libdm ioctl/libdevmapper.a + install -Dm644 libdm/libdevmapper.h $_p/include/libdevmapper.h + install -Dm644 libdm/ioctl/libdevmapper.a $_p/lib/libdevmapper.a + install -Dm644 libdm/libdevmapper.pc $_p/lib/pkgconfig/devmapper.pc + + # configure insists on popt even with every tool disabled; only the lib is built + cd $top/cryptsetup-2.8.8 + CPPFLAGS="-I$_p/include $CPPFLAGS" LDFLAGS="-L$_p/lib $LDFLAGS" \ + ./configure --prefix=$_p --disable-shared --enable-static \ + --with-crypto_backend=kernel \ + --enable-internal-argon2 --enable-internal-sse-argon2 --disable-libargon2 \ + --disable-blkid --disable-udev --disable-selinux \ + --disable-external-tokens --disable-ssh-token --disable-hw-opal \ + --disable-cryptsetup --disable-veritysetup --disable-integritysetup \ + --disable-pwquality --disable-asciidoc --disable-nls + make libcryptsetup.la + make install-libLTLIBRARIES install-includeHEADERS install-pkgconfigDATA + + cd $top + $CC $CFLAGS -I$_p/include -static -o tinyrd tinyrd.c \ + -L$_p/lib $LDFLAGS $(pkg-config --static --libs libcryptsetup) + ./tinyrd -o tinyrd.img + install -Dm644 tinyrd.img $PKG/boot/tinyrd.img +} + +signify() { + untrusted comment: public key + RWTZ9IduCSQ/mL8337TEUinPwT92xFEUpD92hkS7IxcOnzTt9QdpohT3 +} + +# vim: filetype=sh diff --git a/opt/tinyrd/tinyrd.c b/opt/tinyrd/tinyrd.c new file mode 100644 index 0000000..d89e385 --- /dev/null +++ b/opt/tinyrd/tinyrd.c @@ -0,0 +1,526 @@ +/* + * tinyrd — the rawnix initramfs. + * + * One file, one static binary, two roles: + * + * tinyrd -o FILE write a newc cpio whose /init is this binary + * /init (pid 1) optionally unlock a LUKS root, mount root, switch_root + * + * The archive carries no kernel modules, so one image serves every kernel + * and nothing has to be regenerated per version. The price is that the + * unlock path must be built in: + * + * BLK_DEV_INITRD DEVTMPFS BLK_DEV_DM DM_CRYPT CRYPTO_XTS + * CRYPTO_AES_NI_INTEL CRYPTO_SHA256 CRYPTO_HMAC + * CRYPTO_USER_API_SKCIPHER CRYPTO_USER_API_HASH, the root filesystem, + * and whatever the keyboard needs (i8042/atkbd, or xhci + usbhid). + * + * libcryptsetup here uses the kernel crypto backend: every hash goes + * through AF_ALG. Without CRYPTO_USER_API_HASH the header will not load + * ("Cannot initialize crypto backend"). + * + * Kernel command line: + * + * root=/dev/... | UUID=<fs uuid> + * required. A device path is what the + * kernel would use; UUID= survives the + * disk being renamed, which /dev/sda2 + * does not — adding a drive, moving a + * cable or booting the same image in a + * VM is enough + * cryptroot=UUID=<luks-uuid> optional; root= must then be + * /dev/mapper/<name>, and <name> is the + * mapping created + * rootfstype= default ext4 + * rootflags= passed to mount(2) as data + * ro | rw default ro, last one wins + * init= default /sbin/init + * + * Without cryptroot= it mounts root= directly, so the same image boots an + * unencrypted system. + * + * After the switch the new root sees exactly what the kernel leaves it + * when there is no initramfs: devtmpfs on /dev, root mounted ro, nothing + * else. Stage 1 needs no initramfs awareness. + * + * Build: see the port. It links a private static libcryptsetup and + * libdevmapper; static is mandatory and the builder refuses otherwise. + */ + +#define _GNU_SOURCE +#include <dirent.h> +#include <elf.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <termios.h> +#include <time.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/reboot.h> +#include <sys/stat.h> + +#include <libcryptsetup.h> + +#define NEWROOT "/newroot" +#define WAIT_SECS 30 + +/* ── builder ─────────────────────────────────────────────────────── */ + +/* Parents before children: the kernel's unpacker does not create them. */ +static const struct { + const char *name; + unsigned mode, rmaj, rmin; +} skel[] = { + { "dev", S_IFDIR | 0755, 0, 0 }, + { "dev/console", S_IFCHR | 0600, 5, 1 }, /* kernel opens it before /init */ + { "newroot", S_IFDIR | 0755, 0, 0 }, + { "proc", S_IFDIR | 0555, 0, 0 }, + { "run", S_IFDIR | 0755, 0, 0 }, + { "sys", S_IFDIR | 0555, 0, 0 }, +}; + +static void pad4(FILE *f, size_t n) +{ + static const char z[3]; + fwrite(z, 1, (4 - n % 4) % 4, f); +} + +/* uid/gid 0, mtime 0: the image is reproducible and needs no root to build */ +static void cpio_entry(FILE *f, unsigned ino, unsigned mode, unsigned rmaj, + unsigned rmin, const char *name, const void *data, + unsigned size) +{ + unsigned nsz = strlen(name) + 1; + + fprintf(f, "070701%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X", + ino, mode, 0u, 0u, S_ISDIR(mode) ? 2u : 1u, 0u, size, + 0u, 0u, rmaj, rmin, nsz, 0u); + fwrite(name, 1, nsz, f); + pad4(f, 110 + nsz); + if (size) { + fwrite(data, 1, size, f); + pad4(f, size); + } +} + +static unsigned char *slurp(const char *path, unsigned *size) +{ + unsigned char *buf = NULL; + struct stat st; + size_t off = 0; + ssize_t r; + int fd; + + if ((fd = open(path, O_RDONLY | O_CLOEXEC)) < 0) + return NULL; + if (fstat(fd, &st) == 0 && st.st_size > 0 && (buf = malloc(st.st_size))) + while (off < (size_t)st.st_size) { + if ((r = read(fd, buf + off, st.st_size - off)) <= 0) { + free(buf); + buf = NULL; + break; + } + off += r; + } + close(fd); + *size = off; + return buf; +} + +/* + * A dynamic /init fails with ENOENT (its interpreter is missing), and the + * kernel reports that as a panic nobody connects to the link flags. + */ +static int is_static(const unsigned char *b, unsigned n) +{ + Elf64_Ehdr eh; + Elf64_Phdr ph; + unsigned i; + + if (n < sizeof eh || memcmp(b, ELFMAG, SELFMAG) || b[EI_CLASS] != ELFCLASS64) + return 0; + memcpy(&eh, b, sizeof eh); + for (i = 0; i < eh.e_phnum; i++) { + size_t off = eh.e_phoff + (size_t)i * eh.e_phentsize; + if (off + sizeof ph > n) + return 0; + memcpy(&ph, b + off, sizeof ph); + if (ph.p_type == PT_INTERP) + return 0; + } + return 1; +} + +static int build(const char *out, const char *argv0) +{ + unsigned char *self; + unsigned size, i, ino = 1; + char tmp[4096]; + FILE *f; + int bad; + + /* argv[0] covers a build sandbox that hides /proc */ + if (!(self = slurp("/proc/self/exe", &size)) && !(self = slurp(argv0, &size))) { + perror("tinyrd: cannot read own executable"); + return 1; + } + if (!is_static(self, size)) { + fputs("tinyrd: not a static binary; the initramfs has no libc\n", stderr); + return 1; + } + snprintf(tmp, sizeof tmp, "%s.tmp", out); + if (!(f = fopen(tmp, "wb"))) { + perror(tmp); + return 1; + } + for (i = 0; i < sizeof skel / sizeof *skel; i++) + cpio_entry(f, ino++, skel[i].mode, skel[i].rmaj, skel[i].rmin, + skel[i].name, NULL, 0); + cpio_entry(f, ino++, S_IFREG | 0755, 0, 0, "init", self, size); + cpio_entry(f, 0, 0, 0, 0, "TRAILER!!!", NULL, 0); + + bad = ferror(f); + bad |= fclose(f) != 0; + if (bad || rename(tmp, out) < 0) { + perror(out); + unlink(tmp); + return 1; + } + return 0; +} + +/* ── init ────────────────────────────────────────────────────────── */ + +static char cmdbuf[4096]; +static char *tok[256]; +static int ntok; +static char founddev[288]; + +static void say(const char *fmt, ...) +{ + va_list ap; + + fputs("tinyrd: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +/* pid 1 must never return: that is a panic with the message scrolled away */ +static void fatal(const char *fmt, ...) +{ + va_list ap; + char c; + + fputs("tinyrd: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputs("\ntinyrd: press Enter to reboot\n", stderr); + if (read(0, &c, 1) == 1) { + sync(); + reboot(RB_AUTOBOOT); + } + for (;;) + pause(); +} + +static void read_cmdline(void) +{ + ssize_t n = -1; + char *s; + int fd; + + if ((fd = open("/proc/cmdline", O_RDONLY | O_CLOEXEC)) >= 0) { + n = read(fd, cmdbuf, sizeof cmdbuf - 1); + close(fd); + } + if (n < 0) + fatal("cannot read /proc/cmdline"); + cmdbuf[n] = '\0'; + for (s = strtok(cmdbuf, " \t\n"); s && ntok < 256; s = strtok(NULL, " \t\n")) + tok[ntok++] = s; +} + +/* last key=value wins, as in the kernel */ +static const char *param(const char *key) +{ + size_t k = strlen(key); + const char *v = NULL; + int i; + + for (i = 0; i < ntok; i++) + if (!strncmp(tok[i], key, k) && tok[i][k] == '=') + v = tok[i] + k + 1; + return v; +} + +static int want_rw(void) +{ + int i, rw = 0; + + for (i = 0; i < ntok; i++) + if (!strcmp(tok[i], "rw")) + rw = 1; + else if (!strcmp(tok[i], "ro")) + rw = 0; + return rw; +} + +/* + * Filesystem UUIDs, so root= need not name a device. Each entry is a + * magic to confirm the type and the offset of the 16 raw bytes; there + * is no blkid here and no udev to have populated /dev/disk/by-uuid. + */ +static const struct { + off_t moff; + const char *magic; + size_t mlen; + off_t uoff; +} fs_sigs[] = { + { 1080, "\x53\xEF", 2, 1128 }, /* ext2/3/4 */ + { 0, "XFSB", 4, 32 }, /* xfs */ + { 0x10040, "_BHRfS_M", 8, 0x10020 }, /* btrfs */ +}; + +static int fs_uuid_is(const char *dev, const char *uuid) +{ + unsigned char m[8], u[16]; + char got[37]; + int fd, i, ok = 0; + + if ((fd = open(dev, O_RDONLY | O_NONBLOCK | O_CLOEXEC)) < 0) + return 0; + for (i = 0; !ok && i < (int)(sizeof fs_sigs / sizeof *fs_sigs); i++) { + if (pread(fd, m, fs_sigs[i].mlen, fs_sigs[i].moff) != (ssize_t)fs_sigs[i].mlen || + memcmp(m, fs_sigs[i].magic, fs_sigs[i].mlen) || + pread(fd, u, sizeof u, fs_sigs[i].uoff) != (ssize_t)sizeof u) + continue; + snprintf(got, sizeof got, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x%02x%02x%02x%02x", + u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7], + u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]); + ok = !strcasecmp(got, uuid); + } + close(fd); + return ok; +} + +/* LUKS1 and LUKS2 both keep a 40-byte NUL-padded uuid at offset 168 */ +static int luks_uuid_is(const char *dev, const char *uuid) +{ + unsigned char h[208]; + int fd, ok = 0; + + if ((fd = open(dev, O_RDONLY | O_NONBLOCK | O_CLOEXEC)) < 0) + return 0; + if (pread(fd, h, sizeof h, 0) == (ssize_t)sizeof h && + !memcmp(h, "LUKS\xba\xbe", 6)) { + h[207] = '\0'; + ok = !strcasecmp((char *)h + 168, uuid); + } + close(fd); + return ok; +} + +/* one scan for both, since the only difference is what is compared */ +static int probe_uuid(const char *uuid, int luks) +{ + DIR *d = opendir("/sys/class/block"); + struct dirent *e; + int found = 0; + + if (!d) + return 0; + while (!found && (e = readdir(d))) { + if (e->d_name[0] == '.') + continue; + snprintf(founddev, sizeof founddev, "/dev/%s", e->d_name); + found = luks ? luks_uuid_is(founddev, uuid) + : fs_uuid_is(founddev, uuid); + } + closedir(d); + return found; +} + +static int probe_luks(const char *uuid) { return probe_uuid(uuid, 1); } +static int probe_fs(const char *uuid) { return probe_uuid(uuid, 0); } + +static int probe_node(const char *path) +{ + struct stat st; + + return stat(path, &st) == 0 && S_ISBLK(st.st_mode); +} + +/* nvme and usb probe asynchronously; /init can start before the disk exists */ +static void wait_for(int (*probe)(const char *), const char *arg, const char *what) +{ + struct timespec t = { 0, 100 * 1000 * 1000 }; + int i; + + for (i = 0; !probe(arg); i++) { + if (i == 10) + say("waiting for %s", what); + if (i == WAIT_SECS * 10) + fatal("%s did not appear within %ds", what, WAIT_SECS); + nanosleep(&t, NULL); + } +} + +static ssize_t ask(char *buf, size_t n) +{ + struct termios old, t; + int tty = tcgetattr(0, &old) == 0; + ssize_t r; + + if (tty) { + t = old; + t.c_lflag &= ~ECHO; + t.c_lflag |= ECHONL; + tcsetattr(0, TCSAFLUSH, &t); + } + fputs("Passphrase: ", stderr); + r = read(0, buf, n); + if (tty) + tcsetattr(0, TCSAFLUSH, &old); + if (r <= 0) + return -1; + if (buf[r - 1] == '\n') + r--; + return r; +} + +static void cs_log(int level, const char *msg, void *usrptr) +{ + (void)usrptr; + if (level == CRYPT_LOG_ERROR) + fprintf(stderr, "tinyrd: %s", msg); +} + +static void unlock(const char *dev, const char *name) +{ + struct crypt_device *cd = NULL; + char pass[1024]; + ssize_t n; + int r; + + /* no udev here: libdevmapper must create /dev/mapper/<name> itself */ + setenv("DM_DISABLE_UDEV", "1", 1); + crypt_set_log_callback(NULL, cs_log, NULL); + + if ((r = crypt_init(&cd, dev)) < 0 || (r = crypt_load(cd, CRYPT_LUKS, NULL)) < 0) + fatal("%s: cannot load LUKS header: %s", dev, strerror(-r)); + + /* LUKS2 persistent flags (allow-discards, no_*_workqueue) apply here */ + for (;;) { + if ((n = ask(pass, sizeof pass)) < 0) + fatal("cannot read passphrase from console"); + r = crypt_activate_by_passphrase(cd, name, CRYPT_ANY_SLOT, pass, n, 0); + explicit_bzero(pass, sizeof pass); + if (r >= 0) + break; + if (r != -EPERM) + fatal("activating %s on %s: %s", name, dev, strerror(-r)); + say("no key available with this passphrase"); + } + crypt_free(cd); + unsetenv("DM_DISABLE_UDEV"); /* would leak into stage 1's environment */ +} + +static int init_main(char **argv) +{ + const char *root, *cr, *fstype, *init; + char path[4096]; + int fd; + + if (mount("devtmpfs", "/dev", "devtmpfs", 0, NULL) < 0) + fatal("mount /dev: %s", strerror(errno)); + if (fcntl(0, F_GETFD) < 0 && (fd = open("/dev/console", O_RDWR)) >= 0) { + dup2(fd, 0), dup2(fd, 1), dup2(fd, 2); + if (fd > 2) + close(fd); + } + if (mount("proc", "/proc", "proc", MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL) < 0 || + mount("sysfs", "/sys", "sysfs", MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL) < 0 || + mount("tmpfs", "/run", "tmpfs", MS_NOSUID | MS_NODEV, "mode=0755") < 0) + fatal("mount proc/sys/run: %s", strerror(errno)); + + read_cmdline(); + + root = param("root"); + if (!root || (strncmp(root, "/dev/", 5) && strncasecmp(root, "UUID=", 5))) + fatal("root= must be a /dev path or UUID=<fs uuid>, got %s", + root ? root : "nothing"); + + if ((cr = param("cryptroot"))) { + if (strncasecmp(cr, "UUID=", 5) || !cr[5]) + fatal("cryptroot= takes UUID=<luks uuid>"); + if (strncmp(root, "/dev/mapper/", 12) || !root[12] || strchr(root + 12, '/')) + fatal("cryptroot= needs root=/dev/mapper/<name>"); + wait_for(probe_luks, cr + 5, cr); + unlock(founddev, root + 12); + } else if (!strncasecmp(root, "UUID=", 5)) { + if (!root[5]) + fatal("root=UUID= needs a uuid"); + wait_for(probe_fs, root + 5, root); + root = founddev; + } else { + wait_for(probe_node, root, root); + } + + fstype = param("rootfstype"); + if (!fstype) + fstype = "ext4"; + if (mount(root, NEWROOT, fstype, want_rw() ? 0 : MS_RDONLY, param("rootflags")) < 0) + fatal("mount %s (%s): %s", root, fstype, strerror(errno)); + + init = param("init"); + if (!init) + init = "/sbin/init"; + snprintf(path, sizeof path, NEWROOT "%s", init); + if (access(path, X_OK) < 0) + fatal("%s: %s on %s", init, strerror(errno), root); + + /* hand over /dev, drop the rest; the new root mounts its own */ + if (mount("/dev", NEWROOT "/dev", NULL, MS_MOVE, NULL) < 0) + fatal("move /dev: %s", strerror(errno)); + umount2("/run", MNT_DETACH); + umount2("/sys", MNT_DETACH); + umount2("/proc", MNT_DETACH); + + /* free the rootfs; we wrote the archive, so we know what is in it */ + unlink("/init"); + unlink("/dev/console"); + rmdir("/dev"); + rmdir("/proc"); + rmdir("/sys"); + rmdir("/run"); + + if (chdir(NEWROOT) < 0 || mount(".", "/", NULL, MS_MOVE, NULL) < 0 || + chroot(".") < 0 || chdir("/") < 0) + fatal("switch_root: %s", strerror(errno)); + + /* same argv the kernel would have given it: bare words like "single" */ + argv[0] = (char *)init; + execv(init, argv); + fatal("exec %s: %s", init, strerror(errno)); + return 1; +} + +int main(int argc, char **argv) +{ + if (getpid() == 1) + return init_main(argv); + if (argc == 3 && !strcmp(argv[1], "-o")) + return build(argv[2], argv[0]); + fputs("usage: tinyrd -o FILE\n", stderr); + return 2; +} |
