summaryrefslogtreecommitdiffstats
path: root/src/creds
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:49:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:49:52 +0000
commit55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch)
tree33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/creds
parentInitial commit. (diff)
downloadsystemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz
systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/creds')
-rw-r--r--src/creds/creds.c967
-rw-r--r--src/creds/meson.build25
2 files changed, 992 insertions, 0 deletions
diff --git a/src/creds/creds.c b/src/creds/creds.c
new file mode 100644
index 0000000..10d1171
--- /dev/null
+++ b/src/creds/creds.c
@@ -0,0 +1,967 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "build.h"
+#include "creds-util.h"
+#include "dirent-util.h"
+#include "escape.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "hexdecoct.h"
+#include "io-util.h"
+#include "json.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "missing_magic.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "terminal-util.h"
+#include "tpm2-pcr.h"
+#include "tpm2-util.h"
+#include "verbs.h"
+
+typedef enum TranscodeMode {
+ TRANSCODE_OFF,
+ TRANSCODE_BASE64,
+ TRANSCODE_UNBASE64,
+ TRANSCODE_HEX,
+ TRANSCODE_UNHEX,
+ _TRANSCODE_MAX,
+ _TRANSCODE_INVALID = -EINVAL,
+} TranscodeMode;
+
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_system = false;
+static TranscodeMode arg_transcode = TRANSCODE_OFF;
+static int arg_newline = -1;
+static sd_id128_t arg_with_key = _CRED_AUTO;
+static const char *arg_tpm2_device = NULL;
+static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
+static char *arg_tpm2_public_key = NULL;
+static uint32_t arg_tpm2_public_key_pcr_mask = UINT32_MAX;
+static char *arg_tpm2_signature = NULL;
+static const char *arg_name = NULL;
+static bool arg_name_any = false;
+static usec_t arg_timestamp = USEC_INFINITY;
+static usec_t arg_not_after = USEC_INFINITY;
+static bool arg_pretty = false;
+static bool arg_quiet = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep);
+
+static const char* transcode_mode_table[_TRANSCODE_MAX] = {
+ [TRANSCODE_OFF] = "off",
+ [TRANSCODE_BASE64] = "base64",
+ [TRANSCODE_UNBASE64] = "unbase64",
+ [TRANSCODE_HEX] = "hex",
+ [TRANSCODE_UNHEX] = "unhex",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(transcode_mode, TranscodeMode);
+
+static int open_credential_directory(
+ bool encrypted,
+ DIR **ret_dir,
+ const char **ret_prefix) {
+
+ const char *p;
+ DIR *d;
+ int r;
+
+ assert(ret_dir);
+
+ if (arg_system)
+ /* PID 1 ensures that system credentials are always accessible under the same fixed path. It
+ * will create symlinks if necessary to guarantee that. */
+ p = encrypted ?
+ ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY :
+ SYSTEM_CREDENTIALS_DIRECTORY;
+ else {
+ /* Otherwise take the dirs from the env vars we got passed */
+ r = (encrypted ? get_encrypted_credentials_dir : get_credentials_dir)(&p);
+ if (r == -ENXIO) /* No environment variable? */
+ goto not_found;
+ if (r < 0)
+ return log_error_errno(r, "Failed to get credentials directory: %m");
+ }
+
+ d = opendir(p);
+ if (!d) {
+ /* No such dir? Then no creds where passed. (We conditionalize this on arg_system, since for
+ * the per-service case a non-existing path would indicate an issue since the env var would
+ * be set incorrectly in that case.) */
+ if (arg_system && errno == ENOENT)
+ goto not_found;
+
+ return log_error_errno(errno, "Failed to open credentials directory '%s': %m", p);
+ }
+
+ *ret_dir = d;
+
+ if (ret_prefix)
+ *ret_prefix = p;
+
+ return 1;
+
+not_found:
+ *ret_dir = NULL;
+
+ if (ret_prefix)
+ *ret_prefix = NULL;
+
+ return 0;
+}
+
+static int add_credentials_to_table(Table *t, bool encrypted) {
+ _cleanup_closedir_ DIR *d = NULL;
+ const char *prefix;
+ int r;
+
+ assert(t);
+
+ r = open_credential_directory(encrypted, &d, &prefix);
+ if (r < 0)
+ return r;
+ if (!d)
+ return 0; /* No creds dir set */
+
+ for (;;) {
+ _cleanup_free_ char *j = NULL;
+ const char *secure, *secure_color = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ struct dirent *de;
+ struct stat st;
+
+ errno = 0;
+ de = readdir_no_dot(d);
+ if (!de) {
+ if (errno == 0)
+ break;
+
+ return log_error_errno(errno, "Failed to read credentials directory: %m");
+ }
+
+ if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN))
+ continue;
+
+ if (!credential_name_valid(de->d_name))
+ continue;
+
+ fd = openat(dirfd(d), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW);
+ if (fd < 0) {
+ if (errno == ENOENT) /* Vanished by now? */
+ continue;
+
+ return log_error_errno(errno, "Failed to open credential '%s': %m", de->d_name);
+ }
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat credential '%s': %m", de->d_name);
+
+ if (!S_ISREG(st.st_mode))
+ continue;
+
+ if (encrypted) {
+ secure = "encrypted";
+ secure_color = ansi_highlight_green();
+ } else if ((st.st_mode & 0377) != 0) {
+ secure = "insecure"; /* Anything that is accessible more than read-only to its owner is insecure */
+ secure_color = ansi_highlight_red();
+ } else {
+ r = fd_is_fs_type(fd, RAMFS_MAGIC);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine backing file system of '%s': %m", de->d_name);
+
+ secure = r > 0 ? "secure" : "weak"; /* ramfs is not swappable, hence "secure", everything else is "weak" */
+ secure_color = r > 0 ? ansi_highlight_green() : ansi_highlight_yellow4();
+ }
+
+ j = path_join(prefix, de->d_name);
+ if (!j)
+ return log_oom();
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, de->d_name,
+ TABLE_STRING, secure,
+ TABLE_SET_COLOR, secure_color,
+ TABLE_SIZE, (uint64_t) st.st_size,
+ TABLE_STRING, j);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ return 1; /* Creds dir set */
+}
+
+static int verb_list(int argc, char **argv, void *userdata) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ int r, q;
+
+ t = table_new("name", "secure", "size", "path");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 100);
+
+ r = add_credentials_to_table(t, /* encrypted= */ true);
+ if (r < 0)
+ return r;
+
+ q = add_credentials_to_table(t, /* encrypted= */ false);
+ if (q < 0)
+ return q;
+
+ if (r == 0 && q == 0) {
+ if (arg_system)
+ return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed to system.");
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed. (i.e. $CREDENTIALS_DIRECTORY not set.)");
+ }
+
+ if ((arg_json_format_flags & JSON_FORMAT_OFF) && table_get_rows(t) <= 1) {
+ log_info("No credentials");
+ return 0;
+ }
+
+ return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static int transcode(
+ const void *input,
+ size_t input_size,
+ void **ret_output,
+ size_t *ret_output_size) {
+
+ int r;
+
+ assert(input);
+ assert(input_size);
+ assert(ret_output);
+ assert(ret_output_size);
+
+ switch (arg_transcode) {
+
+ case TRANSCODE_BASE64: {
+ char *buf;
+ ssize_t l;
+
+ l = base64mem_full(input, input_size, 79, &buf);
+ if (l < 0)
+ return l;
+
+ *ret_output = buf;
+ *ret_output_size = l;
+ return 0;
+ }
+
+ case TRANSCODE_UNBASE64:
+ r = unbase64mem_full(input, input_size, true, ret_output, ret_output_size);
+ if (r == -EPIPE) /* Uneven number of chars */
+ return -EINVAL;
+
+ return r;
+
+ case TRANSCODE_HEX: {
+ char *buf;
+
+ buf = hexmem(input, input_size);
+ if (!buf)
+ return -ENOMEM;
+
+ *ret_output = buf;
+ *ret_output_size = input_size * 2;
+ return 0;
+ }
+
+ case TRANSCODE_UNHEX:
+ r = unhexmem_full(input, input_size, true, ret_output, ret_output_size);
+ if (r == -EPIPE) /* Uneven number of chars */
+ return -EINVAL;
+
+ return r;
+
+ default:
+ assert_not_reached();
+ }
+}
+
+static int print_newline(FILE *f, const char *data, size_t l) {
+ int fd;
+
+ assert(f);
+ assert(data || l == 0);
+
+ /* If turned off explicitly, don't print newline */
+ if (arg_newline == 0)
+ return 0;
+
+ /* If data already has newline, don't print either */
+ if (l > 0 && data[l-1] == '\n')
+ return 0;
+
+ /* Don't bother unless this is a tty */
+ fd = fileno(f);
+ if (fd >= 0 && isatty(fd) <= 0)
+ return 0;
+
+ if (fputc('\n', f) != '\n')
+ return log_error_errno(errno, "Failed to write trailing newline: %m");
+
+ return 1;
+}
+
+static int write_blob(FILE *f, const void *data, size_t size) {
+ _cleanup_(erase_and_freep) void *transcoded = NULL;
+ int r;
+
+ if (arg_transcode == TRANSCODE_OFF &&
+ arg_json_format_flags != JSON_FORMAT_OFF) {
+ _cleanup_(erase_and_freep) char *suffixed = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ r = make_cstring(data, size, MAKE_CSTRING_REFUSE_TRAILING_NUL, &suffixed);
+ if (r < 0)
+ return log_error_errno(r, "Unable to convert binary string to C string: %m");
+
+ r = json_parse(suffixed, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON: %m");
+
+ json_variant_dump(v, arg_json_format_flags, f, NULL);
+ return 0;
+ }
+
+ if (arg_transcode != TRANSCODE_OFF) {
+ r = transcode(data, size, &transcoded, &size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to transcode data: %m");
+
+ data = transcoded;
+ }
+
+ if (fwrite(data, 1, size, f) != size)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write credential data.");
+
+ r = print_newline(f, data, size);
+ if (r < 0)
+ return r;
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to flush output: %m");
+
+ return 0;
+}
+
+static int verb_cat(int argc, char **argv, void *userdata) {
+ usec_t timestamp;
+ int r, ret = 0;
+
+ timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME);
+
+ STRV_FOREACH(cn, strv_skip(argv, 1)) {
+ _cleanup_(erase_and_freep) void *data = NULL;
+ size_t size = 0;
+ int encrypted;
+
+ if (!credential_name_valid(*cn)) {
+ log_error("Credential name '%s' is not valid.", *cn);
+ if (ret >= 0)
+ ret = -EINVAL;
+ continue;
+ }
+
+ /* Look both in regular and in encrypted credentials */
+ for (encrypted = 0; encrypted < 2; encrypted++) {
+ _cleanup_closedir_ DIR *d = NULL;
+
+ r = open_credential_directory(encrypted, &d, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open credentials directory: %m");
+ if (!d) /* Not set */
+ continue;
+
+ r = read_full_file_full(
+ dirfd(d), *cn,
+ UINT64_MAX, SIZE_MAX,
+ READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE,
+ NULL,
+ (char**) &data, &size);
+ if (r == -ENOENT) /* Not found */
+ continue;
+ if (r >= 0) /* Found */
+ break;
+
+ log_error_errno(r, "Failed to read credential '%s': %m", *cn);
+ if (ret >= 0)
+ ret = r;
+ }
+
+ if (encrypted >= 2) { /* Found nowhere */
+ log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Credential '%s' not set.", *cn);
+ if (ret >= 0)
+ ret = -ENOENT;
+
+ continue;
+ }
+
+ if (encrypted) {
+ _cleanup_(erase_and_freep) void *plaintext = NULL;
+ size_t plaintext_size;
+
+ r = decrypt_credential_and_warn(
+ *cn,
+ timestamp,
+ arg_tpm2_device,
+ arg_tpm2_signature,
+ data, size,
+ &plaintext, &plaintext_size);
+ if (r < 0)
+ return r;
+
+ erase_and_free(data);
+ data = TAKE_PTR(plaintext);
+ size = plaintext_size;
+ }
+
+ r = write_blob(stdout, data, size);
+ if (r < 0)
+ return r;
+ }
+
+ return ret;
+}
+
+static int verb_encrypt(int argc, char **argv, void *userdata) {
+ _cleanup_free_ char *base64_buf = NULL, *fname = NULL;
+ _cleanup_(erase_and_freep) char *plaintext = NULL;
+ const char *input_path, *output_path, *name;
+ _cleanup_free_ void *output = NULL;
+ size_t plaintext_size, output_size;
+ ssize_t base64_size;
+ usec_t timestamp;
+ int r;
+
+ assert(argc == 3);
+
+ input_path = empty_or_dash(argv[1]) ? NULL : argv[1];
+
+ if (input_path)
+ r = read_full_file_full(AT_FDCWD, input_path, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &plaintext, &plaintext_size);
+ else
+ r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, &plaintext, &plaintext_size);
+ if (r == -E2BIG)
+ return log_error_errno(r, "Plaintext too long for credential (allowed size: %zu).", (size_t) CREDENTIAL_SIZE_MAX);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read plaintext: %m");
+
+ output_path = empty_or_dash(argv[2]) ? NULL : argv[2];
+
+ if (arg_name_any)
+ name = NULL;
+ else if (arg_name)
+ name = arg_name;
+ else if (output_path) {
+ r = path_extract_filename(output_path, &fname);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from '%s': %m", output_path);
+ if (r == O_DIRECTORY)
+ return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", output_path);
+
+ name = fname;
+ } else {
+ log_warning("No credential name specified, not embedding credential name in encrypted data. (Disable this warning with --name=)");
+ name = NULL;
+ }
+
+ timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME);
+
+ if (arg_not_after != USEC_INFINITY && arg_not_after < timestamp)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid.");
+
+ r = encrypt_credential_and_warn(
+ arg_with_key,
+ name,
+ timestamp,
+ arg_not_after,
+ arg_tpm2_device,
+ arg_tpm2_pcr_mask,
+ arg_tpm2_public_key,
+ arg_tpm2_public_key_pcr_mask,
+ plaintext, plaintext_size,
+ &output, &output_size);
+ if (r < 0)
+ return r;
+
+ base64_size = base64mem_full(output, output_size, arg_pretty ? 69 : 79, &base64_buf);
+ if (base64_size < 0)
+ return base64_size;
+
+ /* Pretty print makes sense only if we're printing stuff to stdout
+ * and if a cred name is provided via --name= (since we can't use
+ * the output file name as the cred name here) */
+ if (arg_pretty && !output_path && name) {
+ _cleanup_free_ char *escaped = NULL, *indented = NULL, *j = NULL;
+
+ escaped = cescape(name);
+ if (!escaped)
+ return log_oom();
+
+ indented = strreplace(base64_buf, "\n", " \\\n ");
+ if (!indented)
+ return log_oom();
+
+ j = strjoin("SetCredentialEncrypted=", escaped, ": \\\n ", indented, "\n");
+ if (!j)
+ return log_oom();
+
+ free_and_replace(base64_buf, j);
+ }
+
+ if (output_path)
+ r = write_string_file(output_path, base64_buf, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE);
+ else
+ r = write_string_stream(stdout, base64_buf, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write result: %m");
+
+ return EXIT_SUCCESS;
+}
+
+static int verb_decrypt(int argc, char **argv, void *userdata) {
+ _cleanup_(erase_and_freep) void *plaintext = NULL;
+ _cleanup_free_ char *input = NULL, *fname = NULL;
+ _cleanup_fclose_ FILE *output_file = NULL;
+ const char *input_path, *output_path, *name;
+ size_t input_size, plaintext_size;
+ usec_t timestamp;
+ FILE *f;
+ int r;
+
+ assert(IN_SET(argc, 2, 3));
+
+ input_path = empty_or_dash(argv[1]) ? NULL : argv[1];
+
+ if (input_path)
+ r = read_full_file_full(AT_FDCWD, argv[1], UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &input, &input_size);
+ else
+ r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, &input, &input_size);
+ if (r == -E2BIG)
+ return log_error_errno(r, "Data too long for encrypted credential (allowed size: %zu).", (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read encrypted credential data: %m");
+
+ output_path = (argc < 3 || empty_or_dash(argv[2])) ? NULL : argv[2];
+
+ if (arg_name_any)
+ name = NULL;
+ else if (arg_name)
+ name = arg_name;
+ else if (input_path) {
+ r = path_extract_filename(input_path, &fname);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from '%s': %m", input_path);
+ if (r == O_DIRECTORY)
+ return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", input_path);
+
+ name = fname;
+ } else {
+ log_warning("No credential name specified, not validating credential name embedded in encrypted data. (Disable this warning with --name=.)");
+ name = NULL;
+ }
+
+ timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME);
+
+ r = decrypt_credential_and_warn(
+ name,
+ timestamp,
+ arg_tpm2_device,
+ arg_tpm2_signature,
+ input, input_size,
+ &plaintext, &plaintext_size);
+ if (r < 0)
+ return r;
+
+ if (output_path) {
+ output_file = fopen(output_path, "we");
+ if (!output_file)
+ return log_error_errno(errno, "Failed to create output file '%s': %m", output_path);
+
+ f = output_file;
+ } else
+ f = stdout;
+
+ r = write_blob(f, plaintext, plaintext_size);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
+
+static int verb_setup(int argc, char **argv, void *userdata) {
+ size_t size;
+ int r;
+
+ r = get_credential_host_secret(CREDENTIAL_SECRET_GENERATE|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED, NULL, &size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to setup credentials host key: %m");
+
+ log_info("%zu byte credentials host key set up.", size);
+
+ return EXIT_SUCCESS;
+}
+
+static int verb_has_tpm2(int argc, char **argv, void *userdata) {
+ Tpm2Support s;
+
+ s = tpm2_support();
+
+ if (!arg_quiet) {
+ if (s == TPM2_SUPPORT_FULL)
+ puts("yes");
+ else if (s == TPM2_SUPPORT_NONE)
+ puts("no");
+ else
+ puts("partial");
+
+ printf("%sfirmware\n"
+ "%sdriver\n"
+ "%ssystem\n"
+ "%ssubsystem\n"
+ "%slibraries\n",
+ plus_minus(s & TPM2_SUPPORT_FIRMWARE),
+ plus_minus(s & TPM2_SUPPORT_DRIVER),
+ plus_minus(s & TPM2_SUPPORT_SYSTEM),
+ plus_minus(s & TPM2_SUPPORT_SUBSYSTEM),
+ plus_minus(s & TPM2_SUPPORT_LIBRARIES));
+ }
+
+ /* Return inverted bit flags. So that TPM2_SUPPORT_FULL becomes EXIT_SUCCESS and the other values
+ * become some reasonable values 1…7. i.e. the flags we return here tell what is missing rather than
+ * what is there, acknowledging the fact that for process exit statuses it is customary to return
+ * zero (EXIT_FAILURE) when all is good, instead of all being bad. */
+ return ~s & TPM2_SUPPORT_FULL;
+}
+
+static int verb_help(int argc, char **argv, void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-creds", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n"
+ "\n%5$sDisplay and Process Credentials.%6$s\n"
+ "\n%3$sCommands:%4$s\n"
+ " list Show installed and available versions\n"
+ " cat CREDENTIAL... Show specified credentials\n"
+ " setup Generate credentials host key, if not existing yet\n"
+ " encrypt INPUT OUTPUT Encrypt plaintext credential file and write to\n"
+ " ciphertext credential file\n"
+ " decrypt INPUT [OUTPUT] Decrypt ciphertext credential file and write to\n"
+ " plaintext credential file\n"
+ " has-tpm2 Report whether TPM2 support is available\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ "\n%3$sOptions:%4$s\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --json=pretty|short|off\n"
+ " Generate JSON output\n"
+ " --system Show credentials passed to system\n"
+ " --transcode=base64|unbase64|hex|unhex\n"
+ " Transcode credential data\n"
+ " --newline=auto|yes|no\n"
+ " Suffix output with newline\n"
+ " -p --pretty Output as SetCredentialEncrypted= line\n"
+ " --name=NAME Override filename included in encrypted credential\n"
+ " --timestamp=TIME Include specified timestamp in encrypted credential\n"
+ " --not-after=TIME Include specified invalidation time in encrypted\n"
+ " credential\n"
+ " --with-key=host|tpm2|host+tpm2|tpm2-absent|auto|auto-initrd\n"
+ " Which keys to encrypt with\n"
+ " -H Shortcut for --with-key=host\n"
+ " -T Shortcut for --with-key=tpm2\n"
+ " --tpm2-device=PATH\n"
+ " Pick TPM2 device\n"
+ " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
+ " Specify TPM2 PCRs to seal against (fixed hash)\n"
+ " --tpm2-public-key=PATH\n"
+ " Specify PEM certificate to seal against\n"
+ " --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n"
+ " Specify TPM2 PCRs to seal against (public key)\n"
+ " --tpm2-signature=PATH\n"
+ " Specify signature for public key PCR policy\n"
+ " -q --quiet Suppress output for 'has-tpm2' verb\n"
+ "\nSee the %2$s for details.\n"
+ , program_invocation_short_name
+ , link
+ , ansi_underline(), ansi_normal()
+ , ansi_highlight(), ansi_normal()
+ );
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_JSON,
+ ARG_SYSTEM,
+ ARG_TRANSCODE,
+ ARG_NEWLINE,
+ ARG_WITH_KEY,
+ ARG_TPM2_DEVICE,
+ ARG_TPM2_PCRS,
+ ARG_TPM2_PUBLIC_KEY,
+ ARG_TPM2_PUBLIC_KEY_PCRS,
+ ARG_TPM2_SIGNATURE,
+ ARG_NAME,
+ ARG_TIMESTAMP,
+ ARG_NOT_AFTER,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "system", no_argument, NULL, ARG_SYSTEM },
+ { "transcode", required_argument, NULL, ARG_TRANSCODE },
+ { "newline", required_argument, NULL, ARG_NEWLINE },
+ { "pretty", no_argument, NULL, 'p' },
+ { "with-key", required_argument, NULL, ARG_WITH_KEY },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS },
+ { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY },
+ { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS },
+ { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE },
+ { "name", required_argument, NULL, ARG_NAME },
+ { "timestamp", required_argument, NULL, ARG_TIMESTAMP },
+ { "not-after", required_argument, NULL, ARG_NOT_AFTER },
+ { "quiet", no_argument, NULL, 'q' },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hHTpq", options, NULL)) >= 0) {
+
+ switch (c) {
+
+ case 'h':
+ return verb_help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+
+ break;
+
+ case ARG_SYSTEM:
+ arg_system = true;
+ break;
+
+ case ARG_TRANSCODE:
+ if (parse_boolean(optarg) == 0) /* If specified as "false", turn transcoding off */
+ arg_transcode = TRANSCODE_OFF;
+ else {
+ TranscodeMode m;
+
+ m = transcode_mode_from_string(optarg);
+ if (m < 0)
+ return log_error_errno(m, "Failed to parse transcode mode: %m");
+
+ arg_transcode = m;
+ }
+
+ break;
+
+ case ARG_NEWLINE:
+ if (isempty(optarg) || streq(optarg, "auto"))
+ arg_newline = -1;
+ else {
+ r = parse_boolean_argument("--newline=", optarg, NULL);
+ if (r < 0)
+ return r;
+
+ arg_newline = r;
+ }
+ break;
+
+ case 'p':
+ arg_pretty = true;
+ break;
+
+ case ARG_WITH_KEY:
+ if (isempty(optarg) || streq(optarg, "auto"))
+ arg_with_key = _CRED_AUTO;
+ else if (streq(optarg, "auto-initrd"))
+ arg_with_key = _CRED_AUTO_INITRD;
+ else if (streq(optarg, "host"))
+ arg_with_key = CRED_AES256_GCM_BY_HOST;
+ else if (streq(optarg, "tpm2"))
+ arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC;
+ else if (streq(optarg, "tpm2-with-public-key"))
+ arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK;
+ else if (STR_IN_SET(optarg, "host+tpm2", "tpm2+host"))
+ arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC;
+ else if (STR_IN_SET(optarg, "host+tpm2-with-public-key", "tpm2-with-public-key+host"))
+ arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK;
+ else if (streq(optarg, "tpm2-absent"))
+ arg_with_key = CRED_AES256_GCM_BY_TPM2_ABSENT;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown key type: %s", optarg);
+
+ break;
+
+ case 'H':
+ arg_with_key = CRED_AES256_GCM_BY_HOST;
+ break;
+
+ case 'T':
+ arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC;
+ break;
+
+ case ARG_TPM2_DEVICE:
+ if (streq(optarg, "list"))
+ return tpm2_list_devices();
+
+ arg_tpm2_device = streq(optarg, "auto") ? NULL : optarg;
+ break;
+
+ case ARG_TPM2_PCRS: /* For fixed hash PCR policies only */
+ r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_tpm2_pcr_mask);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_PUBLIC_KEY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_PUBLIC_KEY_PCRS: /* For public key PCR policies only */
+ r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_tpm2_public_key_pcr_mask);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_SIGNATURE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_NAME:
+ if (isempty(optarg)) {
+ arg_name = NULL;
+ arg_name_any = true;
+ break;
+ }
+
+ if (!credential_name_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", optarg);
+
+ arg_name = optarg;
+ arg_name_any = false;
+ break;
+
+ case ARG_TIMESTAMP:
+ r = parse_timestamp(optarg, &arg_timestamp);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse timestamp: %s", optarg);
+
+ break;
+
+ case ARG_NOT_AFTER:
+ r = parse_timestamp(optarg, &arg_not_after);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --not-after= timestamp: %s", optarg);
+
+ break;
+
+ case 'q':
+ arg_quiet = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ if (arg_tpm2_pcr_mask == UINT32_MAX)
+ arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT;
+ if (arg_tpm2_public_key_pcr_mask == UINT32_MAX)
+ arg_tpm2_public_key_pcr_mask = UINT32_C(1) << TPM2_PCR_KERNEL_BOOT;
+
+ return 1;
+}
+
+static int creds_main(int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "list", VERB_ANY, 1, VERB_DEFAULT, verb_list },
+ { "cat", 2, VERB_ANY, 0, verb_cat },
+ { "encrypt", 3, 3, 0, verb_encrypt },
+ { "decrypt", 2, 3, 0, verb_decrypt },
+ { "setup", VERB_ANY, 1, 0, verb_setup },
+ { "help", VERB_ANY, 1, 0, verb_help },
+ { "has-tpm2", VERB_ANY, 1, 0, verb_has_tpm2 },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return creds_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/creds/meson.build b/src/creds/meson.build
new file mode 100644
index 0000000..8557256
--- /dev/null
+++ b/src/creds/meson.build
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+executables += [
+ executable_template + {
+ 'name' : 'systemd-creds',
+ 'public' : true,
+ 'sources' : files('creds.c'),
+ 'dependencies' : [
+ libopenssl,
+ threads,
+ ],
+ },
+]
+
+# Protecting files from the distro in /usr doesn't make sense since they can be trivially accessed otherwise,
+# so don't restrict the access mode in /usr. That doesn't apply to /etc, so we do restrict the access mode
+# there.
+install_emptydir(credstoredir)
+if install_sysconfdir
+ # Keep in sync with tmpfiles.d/credstore.conf
+ install_emptydir(sysconfdir / 'credstore',
+ install_mode : 'rwx------')
+ install_emptydir(sysconfdir / 'credstore.encrypted',
+ install_mode : 'rwx------')
+endif