/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <unistd.h>

#include "dropin.h"
#include "efivars.h"
#include "generator.h"
#include "initrd-util.h"
#include "log.h"
#include "parse-util.h"
#include "path-util.h"
#include "proc-cmdline.h"
#include "special.h"
#include "tpm2-util.h"

/* A small generator that enqueues tpm2.target as synchronization point if the TPM2 device hasn't shown up
 * yet, but the firmware reports it to exist. This is supposed to deal with systems where the TPM2 driver
 * support is built as kmod and must be loaded before it's ready to be used. The tpm2.target is only enqueued
 * if firmware says there is a TPM2 device, our userspace support for TPM2 is fully available but the TPM2
 * device hasn't shown up in /dev/ yet. */

static const char *arg_dest = NULL;
static int arg_tpm2_wait = -1; /* tri-state: negative → don't know */
static bool arg_tpm2_software_fallback = false;

static int parse_proc_cmdline_item(const char *key, const char *value, void *data) {
        int r;

        assert(key);

        if (proc_cmdline_key_streq(key, "systemd.tpm2_wait")) {
                r = value ? parse_boolean(value) : 1;
                if (r < 0)
                        log_warning_errno(r, "Failed to parse 'systemd.tpm2_wait=' kernel command line argument, ignoring: %s", value);
                else
                        arg_tpm2_wait = r;

        } else if (proc_cmdline_key_streq(key, "systemd.tpm2_software_fallback")) {
                r = value ? parse_boolean(value) : 1;
                if (r < 0)
                        log_warning_errno(r, "Failed to parse 'systemd.tpm2_software_fallback=' kernel command line argument, ignoring: %s", value);
                else
                        arg_tpm2_software_fallback = r;
        }

        return 0;
}

static int generate_tpm_target_symlink(Tpm2Support support, bool software_fallback_enabled) {
        int r;

        if (arg_tpm2_wait == 0) {
                log_debug("Not generating tpm2.target synchronization point, as this was explicitly turned off via kernel command line.");
                return 0;
        }

        if (arg_tpm2_wait < 0 && !software_fallback_enabled) {
                if (FLAGS_SET(support, TPM2_SUPPORT_DRIVER)) {
                        log_debug("Not generating tpm2.target synchronization point, as TPM2 device is already present.");
                        return 0;
                }

                if (!FLAGS_SET(support, TPM2_SUPPORT_FIRMWARE)) {
                        log_debug("Not generating tpm2.target synchronization point, as firmware reports no TPM2 present.");
                        return 0;
                }
        }

        r = generator_add_symlink(arg_dest, SPECIAL_SYSINIT_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/" SPECIAL_TPM2_TARGET);
        if (r < 0)
                return log_error_errno(r, "Failed to hook in tpm2.target: %m");

        return 0;
}

static int generate_swtpm_symlink(Tpm2Support support) {
        int r;

        if (!arg_tpm2_software_fallback)
                return 0;

        if (FLAGS_SET(support, TPM2_SUPPORT_DRIVER) || FLAGS_SET(support, TPM2_SUPPORT_FIRMWARE)) {
                log_debug("Not generating software TPM units, as a TPM2 device is otherwise available.");
                return 0;
        }

        if (!is_efi_boot()) { /* We need the ESP to store the TPM state. */
                log_warning("TPM software fallback requested but not booted in EFI mode, not pulling in software TPM unit.");
                return 0;
        }

        r = find_executable("swtpm", /* ret_filename= */ NULL);
        if (r == -ENOENT) {
                log_warning("TPM software fallback requested but swtpm not available, not pulling in software TPM unit.");
                return 0;
        }
        if (r < 0)
                return log_error_errno(r, "Failed to determine if 'swtpm' is available: %m");

        r = generator_add_symlink(arg_dest, SPECIAL_SYSINIT_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/systemd-tpm2-swtpm.service");
        if (r < 0)
                return log_error_errno(r, "Failed to hook in systemd-tpm2-swtpm.service: %m");

        if (in_initrd())
                /* Order + pull in the early ESP mount so that swtpm has a place to store its data. */
                r = write_drop_in(
                                arg_dest,
                                "systemd-tpm2-swtpm.service",
                                50, "esp",
                                "# Automatically generated by systemd-tpm2-generator\n\n"
                                "[Unit]\n"
                                "Wants=sysefi.mount\n"
                                "After=sysefi.mount\n");
        else
                /* Order (but not pull in) the regular ESP automount so that swtpm has a place to store its
                 * data. Note that it might be mounted to two different places depending on the existence of
                 * XBOOTLDR, hence order after both. */
                r = write_drop_in(
                                arg_dest,
                                "systemd-tpm2-swtpm.service",
                                50, "esp",
                                "# Automatically generated by systemd-tpm2-generator\n\n"
                                "[Unit]\n"
                                "After=boot.automount efi.automount\n");
        if (r < 0)
                return log_error_errno(r, "Failed to hook ESP mount before systemd-tpm2-swtpm.service: %m");

        return 1; /* Tell caller we now created swtpm units */
}

static int generate_now(void) {
        int r;

        /* Let's shortcut things before we check for TPM2 support if no one cares anyway */
        if (arg_tpm2_wait == 0 && !arg_tpm2_software_fallback) {
                log_debug("Not generating tpm2.target synchronization point or activating software TPM, as turned off via kernel command line.");
                return 0;
        }

        /* We are supposed to sync on TPM or do a software fallback, let's first and unconditionally validate this makes sense at
         * all, i.e. if we have a suitable kernel+userspace. */
        Tpm2Support support = tpm2_support();
        if (!FLAGS_SET(support, TPM2_SUPPORT_SYSTEM|TPM2_SUPPORT_SUBSYSTEM|TPM2_SUPPORT_LIBRARIES)) {

                /* Raise log level if things were explicitly configured */
                log_full((arg_tpm2_wait > 0 ||
                          arg_tpm2_software_fallback) ? LOG_NOTICE : LOG_DEBUG,
                          "Not generating tpm2.target synchronization point or activating software TPM, as userspace support for TPM2 is not complete.");
                return 0;
        }

        r = generate_swtpm_symlink(support);
        if (r < 0)
                return r;

        r = generate_tpm_target_symlink(support, /* software_fallback_enabled= */ r > 0);
        if (r < 0)
                return r;

        return 0;
}

static int run(const char *dest, const char *dest_early, const char *dest_late) {
        int r;

        assert_se(arg_dest = dest);

        r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, PROC_CMDLINE_STRIP_RD_PREFIX);
        if (r < 0)
                log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");

        return generate_now();
}

DEFINE_MAIN_GENERATOR_FUNCTION(run);
