]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
pid1: import creds from SMBIOS too, not just qemu's fw_cfg
authorLennart Poettering <lennart@poettering.net>
Wed, 13 Jul 2022 16:26:44 +0000 (18:26 +0200)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 14 Jul 2022 23:31:34 +0000 (08:31 +0900)
This imports credentials also via SMBIOS' "OEM vendor string" section,
similar to the existing import logic from fw_cfg.

Functionality-wise this is very similar to the existing fw_cfg logic,
both of which are easily settable on the qemu command line.

Pros and cons of each:

SMBIOS OEM vendor strings:
   - pro: fast, because memory mapped
   - pro: somewhat VMM independent, at least in theory
   - pro: qemu upstream sees this as the future
   - pro: no additional kernel module needed
   - con: strings only, thus binary data is base64 encoded

fw_cfg:
   - pro: has been supported for longer in qemu
   - pro: supports binary data
   - con: slow, because IO port based
   - con: only qemu
   - con: requires qemu_fw_cfg.ko kernel module
   - con: qemu upstream sees this as legacy

docs/CREDENTIALS.md
man/systemd.exec.xml
man/systemd.xml
src/core/import-creds.c
test/TEST-54-CREDS/test.sh
test/units/testsuite-54.sh

index 766d84fdba9fcc70eb21c4e712101264e56d5029..bbd92ad3c9f41cb3e056424ca60183c01877cb68 100644 (file)
@@ -50,10 +50,11 @@ purpose. Specifically, the following features are provided:
 6. Service credentials are placed in non-swappable memory. (If permissions
    allow it, via `ramfs`.)
 
-7. Credentials may be acquired from a hosting VM hypervisor (qemu `fw_cfg`), a
-   hosting container manager, the kernel command line, or from the UEFI
-   environment and the EFI System Partition (via `systemd-stub`). Such system
-   credentials may then be propagated into individual services as needed.
+7. Credentials may be acquired from a hosting VM hypervisor (SMBIOS OEM strings
+   or qemu `fw_cfg`), a hosting container manager, the kernel command line, or
+   from the UEFI environment and the EFI System Partition (via
+   `systemd-stub`). Such system credentials may then be propagated into
+   individual services as needed.
 
 8. Credentials are an effective way to pass parameters into services that run
    with `RootImage=` or `RootDirectory=` and thus cannot read these resources
@@ -254,10 +255,18 @@ services where they are ultimately consumed.
    the [Container Interface](CONTAINER_INTERFACE.md)
    documentation.
 
-2. Quite similar, qemu VMs can be invoked with `-fw_cfg
+2. Quite similar, VMs can be passed credentials via SMBIOS OEM strings (example
+   qemu command line switch `-smbios
+   type=11,value=io.systemd.credential:foo=bar` or `-smbios
+   type=11,value=io.systemd.credential.binary:foo=YmFyCg==`, the latter taking
+   a Base64 encoded argument to permit binary credentials being passed
+   in). Alternatively, qemu VMs can be invoked with `-fw_cfg
    name=opt/io.systemd.credentials/foo,string=bar` to pass credentials from
-   host through the hypervisor into the VM. (This specific switch would set
-   credential `foo` to `bar`.)
+   host through the hypervisor into the VM via qemu's `fw_cfg` mechanism. (All
+   three of these specific switches would set credential `foo` to `bar`.)
+   Passing credentials via the SMBIOS mechanism is typically preferable over
+   `fw_cfg` since it is faster and less specific to the chosen VMM
+   implementation.
 
 3. Credentials can also be passed into a system via the kernel command line,
    via the `systemd.set-credential=` kernel command line option. Note though
@@ -297,7 +306,7 @@ qemu-system-x86_64 \
         -drive if=none,id=hd,file=test.raw,format=raw \
         -device virtio-scsi-pci,id=scsi \
         -device scsi-hd,drive=hd,bootindex=1 \
-        -fw_cfg name=opt/io.systemd.credentials/mycred,string=supersecret
+        -smbios type=11,value=io.systemd.credential:mycred=supersecret
 ```
 
 Either of these lines will boot a disk image `test.raw`, once as container via
@@ -363,8 +372,8 @@ qemu-system-x86_64 \
         -drive if=none,id=hd,file=test.raw,format=raw \
         -device virtio-scsi-pci,id=scsi \
         -device scsi-hd,drive=hd,bootindex=1 \
-        -fw_cfg name=opt/io.systemd.credentials/passwd.hashed-password.root,string=$(mkpasswd mysecret) \
-        -fw_cfg name=opt/io.systemd.credentials/firstboot.locale,string=C.UTF-8
+        -smbios type=11,value=io.systemd.credential:passwd.hashed-password.root=$(mkpasswd mysecret) \
+        -smbios type=11,value=io.systemd.credential:firstboot.locale=C.UTF-8
 ```
 
 ## Relevant Paths
index 3d7ec1e2028175c7899bddffee6271c07ee8e601..055858ef044d3da35cc53bde2f34b1664808ef39 100644 (file)
@@ -3125,12 +3125,20 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX
         <para>The service manager itself may receive system credentials that can be propagated to services
         from a hosting container manager or VM hypervisor. See the <ulink
         url="https://systemd.io/CONTAINER_INTERFACE">Container Interface</ulink> documentation for details
-        about the former. For the latter, use the <command>qemu</command> <literal>fw_cfg</literal> node
+        about the former. For the latter, pass <ulink
+        url="https://www.dmtf.org/standards/smbios">DMI/SMBIOS</ulink> OEM string table entries (field type
+        11) with a prefix of <literal>io.systemd.credential:</literal> or
+        <literal>io.systemd.credential.binary:</literal>. In both cases a key/value pair separated by
+        <literal>=</literal> is expected, in the latter case the right-hand side is Base64 decoded when
+        parsed (thus permitting binary data to be passed in). Example qemu switch: <literal>-smbios
+        type=11,value=io.systemd.credential:xx=yy</literal>, or <literal>-smbios
+        type=11,value=io.systemd.credential.binary:rick=TmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXA=</literal>. Alternatively,
+        use the <command>qemu</command> <literal>fw_cfg</literal> node
         <literal>opt/io.systemd.credentials/</literal>. Example qemu switch: <literal>-fw_cfg
         name=opt/io.systemd.credentials/mycred,string=supersecret</literal>. They may also be specified on
         the kernel command line using the <literal>systemd.set_credential=</literal> switch (see
-        <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
-        and from the UEFI firmware environment via
+        <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>) and from
+        the UEFI firmware environment via
         <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
 
         <para>If referencing an <constant>AF_UNIX</constant> stream socket to connect to, the connection will
index e526a1caea06d4742cc090622b96a6e86607bd30..30484e09a9b01b81d1c850aba6587840370f340b 100644 (file)
         <term><varname>systemd.import_credentials=</varname></term>
 
         <listitem><para>Takes a boolean argument. If false disables importing credentials from the kernel
-        command line, qemu_fw_cfg subsystem or the kernel command line.</para></listitem>
+        command line, the DMI/SMBIOS OEM string table, the qemu_fw_cfg subsystem or the EFI kernel
+        stub.</para></listitem>
       </varlistentry>
 
       <varlistentry>
index 53796484ee3f6457b836940ebe60a9c6e53afece..3324a6eaab1f22715e4e8179523ca114a38f5ac4 100644 (file)
@@ -4,9 +4,11 @@
 
 #include "copy.h"
 #include "creds-util.h"
+#include "escape.h"
 #include "fileio.h"
 #include "format-util.h"
 #include "fs-util.h"
+#include "hexdecoct.h"
 #include "import-creds.h"
 #include "io-util.h"
 #include "mkdir-label.h"
@@ -24,7 +26,7 @@
  * generators invoked by it) can acquire credentials from outside, to mimic how we support it for containers,
  * but on VM/physical environments.
  *
- * This does three things:
+ * This does four things:
  *
  * 1. It imports credentials picked up by sd-boot (and placed in the /.extra/credentials/ dir in the initrd)
  *    and puts them in /run/credentials/@encrypted/. Note that during the initrd→host transition the initrd root
  *    /sys/firmware/qemu_fw_cfg/by_name/opt/io.systemd.credentials/ is picked up and also placed in
  *    /run/credentials/@system/.
  *
+ * 4. It imports credentials passed in via the DMI/SMBIOS OEM string tables, quite similar to fw_cfg. It
+ *    looks for strings starting with "io.systemd.credential:" and "io.systemd.credential.binary:". Both
+ *    expect a key=value assignment, but in the latter case the value is Base64 decoded, allowing binary
+ *    credentials to be passed in.
+ *
  * If it picked up any credentials it will set the $CREDENTIALS_DIRECTORY and
  * $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables to point to these directories, so that processes
  * can find them there later on. If "ramfs" is available $CREDENTIALS_DIRECTORY will be backed by it (but
@@ -446,26 +453,177 @@ static int import_credentials_qemu(ImportCredentialContext *c) {
         return 0;
 }
 
+static int parse_smbios_strings(ImportCredentialContext *c, const char *data, size_t size) {
+        size_t left, skip;
+        const char *p;
+        int r;
+
+        assert(c);
+        assert(data || size == 0);
+
+        /* Unpacks a packed series of SMBIOS OEM vendor strings. These are a series of NUL terminated
+         * strings, one after the other. */
+
+        for (p = data, left = size; left > 0; p += skip, left -= skip) {
+                _cleanup_free_ void *buf = NULL;
+                _cleanup_free_ char *cn = NULL;
+                _cleanup_close_ int nfd = -1;
+                const char *nul, *n, *eq;
+                const void *cdata;
+                size_t buflen, cdata_len;
+                bool unbase64;
+
+                nul = memchr(p, 0, left);
+                if (nul)
+                        skip = (nul - p) + 1;
+                else {
+                        nul = p + left;
+                        skip = left;
+                }
+
+                if (nul - p == 0) /* Skip empty strings */
+                        continue;
+
+                /* Only care about strings starting with either of these two prefixes */
+                if ((n = memory_startswith(p, nul - p, "io.systemd.credential:")))
+                        unbase64 = false;
+                else if ((n = memory_startswith(p, nul - p, "io.systemd.credential.binary:")))
+                        unbase64 = true;
+                else {
+                        _cleanup_free_ char *escaped = NULL;
+
+                        escaped = cescape_length(p, nul - p);
+                        log_debug("Ignoring OEM string: %s", strnull(escaped));
+                        continue;
+                }
+
+                eq = memchr(n, '=', nul - n);
+                if (!eq) {
+                        log_warning("SMBIOS OEM string lacks '=' character, ignoring.");
+                        continue;
+                }
+
+                cn = memdup_suffix0(n, eq - n);
+                if (!cn)
+                        return log_oom();
+
+                if (!credential_name_valid(cn)) {
+                        log_warning("SMBIOS credential name '%s' is not valid, ignoring: %m", cn);
+                        continue;
+                }
+
+                /* Optionally base64 decode the data, if requested, to allow binary credentials */
+                if (unbase64) {
+                        r = unbase64mem(eq + 1, nul - (eq + 1), &buf, &buflen);
+                        if (r < 0) {
+                                log_warning_errno(r, "Failed to base64 decode credential '%s', ignoring: %m", cn);
+                                continue;
+                        }
+
+                        cdata = buf;
+                        cdata_len = buflen;
+                } else {
+                        cdata = eq + 1;
+                        cdata_len = nul - (eq + 1);
+                }
+
+                if (!credential_size_ok(c, cn, cdata_len))
+                        continue;
+
+                r = acquire_credential_directory(c);
+                if (r < 0)
+                        return r;
+
+                nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, cn);
+                if (nfd == -EEXIST)
+                        continue;
+                if (nfd < 0)
+                        return nfd;
+
+                r = loop_write(nfd, cdata, cdata_len, /* do_poll= */ false);
+                if (r < 0) {
+                        (void) unlinkat(c->target_dir_fd, cn, 0);
+                        return log_error_errno(r, "Failed to write credential: %m");
+                }
+
+                c->size_sum += cdata_len;
+                c->n_credentials++;
+
+                log_debug("Successfully processed SMBIOS credential '%s'.", cn);
+        }
+
+        return 0;
+}
+
+static int import_credentials_smbios(ImportCredentialContext *c) {
+        int r;
+
+        /* Parses DMI OEM strings fields (SMBIOS type 11), as settable with qemu's -smbios type=11,value=… switch. */
+
+        for (unsigned i = 0;; i++) {
+                struct dmi_field_header {
+                        uint8_t type;
+                        uint8_t length;
+                        uint16_t handle;
+                        uint8_t count;
+                        char contents[];
+                } _packed_ *dmi_field_header;
+                _cleanup_free_ char *p = NULL;
+                _cleanup_free_ void *data = NULL;
+                size_t size;
+
+                assert_cc(offsetof(struct dmi_field_header, contents) == 5);
+
+                if (asprintf(&p, "/sys/firmware/dmi/entries/11-%u/raw", i) < 0)
+                        return log_oom();
+
+                r = read_virtual_file(p, sizeof(dmi_field_header) + CREDENTIALS_TOTAL_SIZE_MAX, (char**) &data, &size);
+                if (r < 0) {
+                        /* Once we reach ENOENT there are no more DMI Type 11 fields around. */
+                        log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, "Failed to open '%p', ignoring: %m", p);
+                        break;
+                }
+
+                if (size < offsetof(struct dmi_field_header, contents))
+                        return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "DMI field header of '%p' too short.", p);
+
+                dmi_field_header = data;
+                if (dmi_field_header->type != 11 ||
+                    dmi_field_header->length != offsetof(struct dmi_field_header, contents))
+                        return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Invalid DMI field header.");
+
+                r = parse_smbios_strings(c, dmi_field_header->contents, size - offsetof(struct dmi_field_header, contents));
+                if (r < 0)
+                        return r;
+
+                if (i == UINT_MAX) /* Prevent overflow */
+                        break;
+        }
+
+        return 0;
+}
+
 static int import_credentials_trusted(void) {
         _cleanup_(import_credentials_context_free) ImportCredentialContext c = {
                 .target_dir_fd = -1,
         };
-        int q, r;
+        int q, w, r;
 
         r = import_credentials_qemu(&c);
+        w = import_credentials_smbios(&c);
         q = import_credentials_proc_cmdline(&c);
 
         if (c.n_credentials > 0) {
                 int z;
 
-                log_debug("Imported %u credentials from kernel command line/fw_cfg.", c.n_credentials);
+                log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg.", c.n_credentials);
 
                 z = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY");
                 if (z < 0)
                         return z;
         }
 
-        return r < 0 ? r : q;
+        return r < 0 ? r : w < 0 ? w : q;
 }
 
 static int symlink_credential_dir(const char *envvar, const char *path, const char *where) {
index 8d5d796cc8b99828dd276046619e209565f69676..7bbc94ebc02f1c4b32278b21ea4c29f85414519d 100755 (executable)
@@ -4,7 +4,7 @@ set -e
 
 TEST_DESCRIPTION="test credentials"
 NSPAWN_ARGUMENTS="${NSPAWN_ARGUMENTS:-} --set-credential=mynspawncredential:strangevalue"
-QEMU_OPTIONS="${QEMU_OPTIONS:-} -fw_cfg  name=opt/io.systemd.credentials/myqemucredential,string=othervalue"
+QEMU_OPTIONS="${QEMU_OPTIONS:-} -fw_cfg name=opt/io.systemd.credentials/myqemucredential,string=othervalue -smbios type=11,value=io.systemd.credential:smbioscredential=magicdata -smbios type=11,value=io.systemd.credential.binary:binarysmbioscredential=bWFnaWNiaW5hcnlkYXRh"
 KERNEL_APPEND="${KERNEL_APPEND:-} systemd.set_credential=kernelcmdlinecred:uff systemd.set_credential=sysctl.extra:kernel.domainname=sysctltest rd.systemd.import_credentials=no"
 
 # shellcheck source=test/test-functions
index 06f3beb287d6d3ab2d78b87879a134d0404f3d8c..151a7987d5a42362242d0af8f6bc5d4208da11d3 100755 (executable)
@@ -29,6 +29,10 @@ elif [ -d /sys/firmware/qemu_fw_cfg/by_name ]; then
     # Verify that passing creds through kernel cmdline works
     [ "$(systemd-creds --system cat kernelcmdlinecred)" = "uff" ]
 
+    # And that it also works via SMBIOS
+    [ "$(systemd-creds --system cat smbioscredential)" = "magicdata" ]
+    [ "$(systemd-creds --system cat binarysmbioscredential)" = "magicbinarydata" ]
+
     # If we aren't run in nspawn, we are run in qemu
     systemd-detect-virt -q -v
     expected_credential=myqemucredential