2018-04-15 19:08:37

by Hansjoerg Lipp

[permalink] [raw]
Subject: [RFC] Passing luks passphrase from grub to systemd

Hello,

as I'm stuck with a (non-EFI x86_64) system with encrypted root
partition, I have to enter the passphrase twice (grub needs it for
getting the kernel etc., systemd needs it for mounting the root
partition). This can be quite inconvenient, especially if the passphrase
is long and contains special characters, and grub assumes a different
keyboard layout.

I therefore developed a proof of concept code allowing grub to pass the
passphrase to the kernel and systemd to get the passphrase from the
kernel. See the description and patch for the Linux part and the link to
all changes below.

I'm presenting my code here because I'd like to know if something like
this might be useful also for other people (and further development
might be sensible). I'd also be thankful for comments how this might be
implemented in a better way.

As I wanted to avoid introducing a new incompatible interface for
passing data to the Linux kernel (all combinations of grub, Linux,
systemd, with and without these changes should still work), I actually
extended the mechanism which is already in place to pass the regular
kernel command line, allowing to reuse existing code in many places.

The secret data is passed as a "hidden command line" following the NUL
character terminating the regular command line. In order to enable Linux
to detect whether a hidden command line exists, it is prefixed with a
signature (currently "hdn "). The hidden command line contains the usual
"key=value" pairs which might be useful fur other purposes; currently,
grub is passing a string "pwd=PASSPHRASE" for every passphrase the user
enters during the boot process. The full format therefore reads
$regular_cmdline NUL "hdn " $hidden_cmdline NUL

The kernel actually does not interpret the data but leaves this task to
the init process. It merely copies the data to a new variable
hidden_command_line which is read when root reads from the new proc fs
entry /proc/cmdline_hidden. The code does ensure that the hidden command
line is zeroed out as soon as it has been read by root. It might also be
a good idea to restrict access to this entry to the init process?

The systemd changes are quite small as there is already code in place
that stores passphrases entered by the user for mounting further
encrypted devices. The passphrases read from /proc/cmdline_hidden just
have to be fed into that mechanism.

The patch for the current stable kernel can be found below. Details and
all code can be found at
http://www.hlipp.de/linux-passphrase-transfer/

Kind regards,
Hansjoerg


fs/proc/cmdline.c | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++
include/linux/init.h | 1
init/main.c | 26 ++++++++++++++++++++--
3 files changed, 85 insertions(+), 2 deletions(-)

--- linux-orig/include/linux/init.h
+++ linux/include/linux/init.h
@@ -126,6 +126,7 @@
extern int do_one_initcall(initcall_t fn);
extern char __initdata boot_command_line[];
extern char *saved_command_line;
+extern char *hidden_command_line;
extern unsigned int reset_devices;

/* used by init/main.c */
--- linux-orig/init/main.c
+++ linux/init/main.c
@@ -129,6 +129,8 @@
char __initdata boot_command_line[COMMAND_LINE_SIZE];
/* Untouched saved command line (eg. for /proc) */
char *saved_command_line;
+/* Saved hidden command line (for /proc; can be 0) */
+char *hidden_command_line;
/* Command line for parameter parsing */
static char *static_command_line;
/* Command line for per-initcall parameter parsing */
@@ -369,13 +371,33 @@
*/
static void __init setup_command_line(char *command_line)
{
+ size_t len = strlen(boot_command_line);
saved_command_line =
- memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
+ memblock_virt_alloc(len + 1, 0);
initcall_command_line =
- memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
+ memblock_virt_alloc(len + 1, 0);
static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0);
strcpy(saved_command_line, boot_command_line);
strcpy(static_command_line, command_line);
+
+ /* Detect hidden command line parameters.
+ * The format is: Regular parameters, 0, "hdn ", hidden parameters, 0
+ */
+ if (len < COMMAND_LINE_SIZE - 6
+ && boot_command_line[len + 1] == 'h'
+ && boot_command_line[len + 2] == 'd'
+ && boot_command_line[len + 3] == 'n'
+ && boot_command_line[len + 4] == ' '
+ ) {
+ size_t len2;
+ boot_command_line[COMMAND_LINE_SIZE - 1] = 0; /* Make sure the string is 0 terminated. */
+ len2 = strlen(boot_command_line + len + 5);
+ hidden_command_line = memblock_virt_alloc(len2 + 1, 0);
+ strcpy(hidden_command_line, boot_command_line + len + 5);
+ memset(boot_command_line + len + 5, 0, len2); /* Zero out hidden parameters. */
+ } else {
+ hidden_command_line = 0;
+ }
}

/*
--- linux-orig/fs/proc/cmdline.c
+++ linux/fs/proc/cmdline.c
@@ -22,9 +23,69 @@
.release = single_release,
};

+static DEFINE_SPINLOCK(cmdline_lock);
+
+static int cmdline_hidden_proc_show(struct seq_file *m, void *v)
+{
+ char *cmdline = 0;
+ size_t len;
+
+ /* Hidden data can only be read once! */
+ spin_lock(&cmdline_lock);
+ if (hidden_command_line) {
+ cmdline = hidden_command_line;
+ hidden_command_line = 0;
+ }
+ spin_unlock(&cmdline_lock);
+
+ /* No hidden command line found, or already read. */
+ if (!cmdline) {
+ seq_printf(m, "\n");
+ return 0;
+ }
+
+ seq_printf(m, "%s\n", cmdline);
+
+ /* Zero out data. */
+ len = strlen(cmdline);
+ memset(cmdline, 0, len);
+ //memblock_free(__pa(cmdline), len + 1); // Should we somehow make this work or just "leak" this memory?
+ //kmalloc()/kfree() is probably also bad in init/main.c:setup_command_line().
+
+ return 0;
+}
+
+static int cmdline_hidden_proc_open(struct inode *inode, struct file *file)
+{
+ /* Only root may open this. */
+ if (!uid_eq(current_euid(), GLOBAL_ROOT_UID)) {
+ //if (!uid_eq(current_euid(), GLOBAL_ROOT_UID) || current->pid != 1) { /* We could also restrict access to the init process. */
+ return -EACCES;
+ }
+ return single_open(file, cmdline_hidden_proc_show, NULL);
+}
+
+static int cmdline_hidden_proc_release(struct inode *inode, struct file *file)
+{
+ /* Zero out data. */
+ struct seq_file *m = file->private_data;
+ if (m->buf) {
+ memset(m->buf, 0, m->size);
+ }
+ return single_release(inode, file);
+}
+
+static const struct file_operations cmdline_hidden_proc_fops = {
+ .open = cmdline_hidden_proc_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = cmdline_hidden_proc_release,
+};
+
static int __init proc_cmdline_init(void)
{
proc_create("cmdline", 0, NULL, &cmdline_proc_fops);
+ proc_create("cmdline_hidden", 0, NULL, &cmdline_hidden_proc_fops);
return 0;
}
fs_initcall(proc_cmdline_init);



2018-04-15 22:26:44

by Oleksandr Natalenko

[permalink] [raw]
Subject: Re: [RFC] Passing luks passphrase from grub to systemd

Hi.

> as I'm stuck with a (non-EFI x86_64) system with encrypted root
> partition, I have to enter the passphrase twice (grub needs it for
> getting the kernel etc., systemd needs it for mounting the root
> partition). This can be quite inconvenient, especially if the passphrase
> is long and contains special characters, and grub assumes a different
> keyboard layout.

Just fill another LUKS slot with a randomly generated key file and add that
file to your initramfs (which already resides on encrypted /boot, right?). If
your distro cannot do that, you should probably fixing things there, not
adding ugly hacks to the kernel.

Check how it is implemented in Arch, for instance [1]. I'm not sure whether
this is currently possible with openSUSE, though.

Regards,
Oleksandr

[1] https://klmlinks.wordpress.com/2016/03/



2018-04-15 22:37:24

by Gabriel C

[permalink] [raw]
Subject: Re: [RFC] Passing luks passphrase from grub to systemd

2018-04-15 21:06 GMT+02:00 Hansjoerg Lipp <[email protected]>:
> Hello,
>

Hello,

> as I'm stuck with a (non-EFI x86_64) system with encrypted root
> partition, I have to enter the passphrase twice (grub needs it for
> getting the kernel etc., systemd needs it for mounting the root
> partition). This can be quite inconvenient, especially if the passphrase
> is long and contains special characters, and grub assumes a different
> keyboard layout.
> I therefore developed a proof of concept code allowing grub to pass the
> passphrase to the kernel and systemd to get the passphrase from the
> kernel. See the description and patch for the Linux part and the link to
> all changes below.
>
> I'm presenting my code here because I'd like to know if something like
> this might be useful also for other people (and further development
> might be sensible). I'd also be thankful for comments how this might be
> implemented in a better way.

Somethng like this is not needed.
All that is possible already from userspace.

Systemd can do that on his own ( see systemd-cryptsetup-generator )
( other init ofc too ) assuming your initrd , cryptsetup and grub is
setup correctly.


Regards,

Gabriel C

2018-04-15 23:25:20

by Hansjoerg Lipp

[permalink] [raw]
Subject: Re: [RFC] Passing luks passphrase from grub to systemd

Hello Gabriel,

On Mon, Apr 16, 2018 at 12:35:33AM +0200, Gabriel C wrote:
> 2018-04-15 21:06 GMT+02:00 Hansjoerg Lipp <[email protected]>:
> > as I'm stuck with a (non-EFI x86_64) system with encrypted root
> > partition, I have to enter the passphrase twice (grub needs it for
> > getting the kernel etc., systemd needs it for mounting the root
> > partition). This can be quite inconvenient, especially if the passphrase
> > is long and contains special characters, and grub assumes a different
> > keyboard layout.
> > I therefore developed a proof of concept code allowing grub to pass the
> > passphrase to the kernel and systemd to get the passphrase from the
> > kernel. See the description and patch for the Linux part and the link to
> > all changes below.
>
> Somethng like this is not needed.
> All that is possible already from userspace.
>
> Systemd can do that on his own ( see systemd-cryptsetup-generator )
> ( other init ofc too ) assuming your initrd , cryptsetup and grub is
> setup correctly.

can this be done even if the /boot directory is located inside the
encrypted root partition? I'll have another look at this tomorrow as I'm
currently too tired. This would be really great news for me. (And I'd
have to wonder how I didn't get this right on my own... I already
learned a lot about the boot process doing this, and obviously I still
do not understand enough.)

Thank you for your quick response,
Hansjoerg

2018-04-16 00:12:50

by Hansjoerg Lipp

[permalink] [raw]
Subject: Re: [RFC] Passing luks passphrase from grub to systemd

Hello Oleksandr.

Am 16.04.2018 um 00:25 schrieb Oleksandr Natalenko:
>> as I'm stuck with a (non-EFI x86_64) system with encrypted root
>> partition, I have to enter the passphrase twice (grub needs it for
>> getting the kernel etc., systemd needs it for mounting the root
>> partition). This can be quite inconvenient, especially if the passphrase
>> is long and contains special characters, and grub assumes a different
>> keyboard layout.
>
> Just fill another LUKS slot with a randomly generated key file and add that
> file to your initramfs (which already resides on encrypted /boot, right?). If
> your distro cannot do that, you should probably fixing things there, not
> adding ugly hacks to the kernel.

Yes, I never considered this proof of concept code as a good solution (I
don't want to get it into the kernel!), it was meant as a starting point
for discussing whether there is need for some mechanism to get data like
this from the boot loader to the init process, and if so, how to do it
right (and it was actually fun to learn a bit about all this).

I'm thankful for your hint how I could solve my personal luks problem in
a clean way (although it somehow does not feel right to have a key file
accessible to probable malware while the machine is running; of course a
paranoid thought of me...).

Kind regards and thanks again
Hansjoerg