]>
Commit | Line | Data |
---|---|---|
5945640e LP |
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | ||
3 | #include <getopt.h> | |
3c9fbb99 | 4 | #include <unistd.h> |
5945640e | 5 | |
d6b4d1c7 | 6 | #include "build.h" |
caef0bc3 | 7 | #include "bus-polkit.h" |
5945640e LP |
8 | #include "creds-util.h" |
9 | #include "dirent-util.h" | |
10 | #include "escape.h" | |
11 | #include "fileio.h" | |
12 | #include "format-table.h" | |
13 | #include "hexdecoct.h" | |
14 | #include "io-util.h" | |
15 | #include "json.h" | |
16 | #include "main-func.h" | |
17 | #include "memory-util.h" | |
18 | #include "missing_magic.h" | |
19 | #include "pager.h" | |
20 | #include "parse-argument.h" | |
21 | #include "pretty-print.h" | |
22 | #include "process-util.h" | |
23 | #include "stat-util.h" | |
24 | #include "string-table.h" | |
25 | #include "terminal-util.h" | |
e0e1f4f7 | 26 | #include "tpm2-pcr.h" |
5945640e | 27 | #include "tpm2-util.h" |
644f19c7 LP |
28 | #include "user-util.h" |
29 | #include "varlink.h" | |
30 | #include "varlink-io.systemd.Credentials.h" | |
5945640e LP |
31 | #include "verbs.h" |
32 | ||
33 | typedef enum TranscodeMode { | |
34 | TRANSCODE_OFF, | |
35 | TRANSCODE_BASE64, | |
36 | TRANSCODE_UNBASE64, | |
37 | TRANSCODE_HEX, | |
38 | TRANSCODE_UNHEX, | |
39 | _TRANSCODE_MAX, | |
40 | _TRANSCODE_INVALID = -EINVAL, | |
41 | } TranscodeMode; | |
42 | ||
43 | static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; | |
44 | static PagerFlags arg_pager_flags = 0; | |
45 | static bool arg_legend = true; | |
46 | static bool arg_system = false; | |
47 | static TranscodeMode arg_transcode = TRANSCODE_OFF; | |
48 | static int arg_newline = -1; | |
571d829e | 49 | static sd_id128_t arg_with_key = _CRED_AUTO; |
5945640e LP |
50 | static const char *arg_tpm2_device = NULL; |
51 | static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; | |
75ddec93 LP |
52 | static char *arg_tpm2_public_key = NULL; |
53 | static uint32_t arg_tpm2_public_key_pcr_mask = UINT32_MAX; | |
54 | static char *arg_tpm2_signature = NULL; | |
5945640e LP |
55 | static const char *arg_name = NULL; |
56 | static bool arg_name_any = false; | |
57 | static usec_t arg_timestamp = USEC_INFINITY; | |
58 | static usec_t arg_not_after = USEC_INFINITY; | |
59 | static bool arg_pretty = false; | |
6e0cb815 | 60 | static bool arg_quiet = false; |
644f19c7 | 61 | static bool arg_varlink = false; |
c85b68f6 | 62 | static uid_t arg_uid = UID_INVALID; |
aadbe559 | 63 | static bool arg_allow_null = false; |
5945640e | 64 | |
75ddec93 LP |
65 | STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); |
66 | STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); | |
67 | ||
5945640e LP |
68 | static const char* transcode_mode_table[_TRANSCODE_MAX] = { |
69 | [TRANSCODE_OFF] = "off", | |
70 | [TRANSCODE_BASE64] = "base64", | |
71 | [TRANSCODE_UNBASE64] = "unbase64", | |
72 | [TRANSCODE_HEX] = "hex", | |
73 | [TRANSCODE_UNHEX] = "unhex", | |
74 | }; | |
75 | ||
76 | DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(transcode_mode, TranscodeMode); | |
77 | ||
05eb896f | 78 | static int open_credential_directory( |
07a442ef | 79 | bool encrypted, |
05eb896f | 80 | DIR **ret_dir, |
07a442ef | 81 | const char **ret_prefix) { |
05eb896f | 82 | |
5945640e LP |
83 | const char *p; |
84 | DIR *d; | |
85 | int r; | |
86 | ||
05eb896f LP |
87 | assert(ret_dir); |
88 | ||
89 | if (arg_system) | |
90 | /* PID 1 ensures that system credentials are always accessible under the same fixed path. It | |
91 | * will create symlinks if necessary to guarantee that. */ | |
92 | p = encrypted ? | |
93 | ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY : | |
94 | SYSTEM_CREDENTIALS_DIRECTORY; | |
95 | else { | |
96 | /* Otherwise take the dirs from the env vars we got passed */ | |
97 | r = (encrypted ? get_encrypted_credentials_dir : get_credentials_dir)(&p); | |
98 | if (r == -ENXIO) /* No environment variable? */ | |
99 | goto not_found; | |
5945640e | 100 | if (r < 0) |
05eb896f LP |
101 | return log_error_errno(r, "Failed to get credentials directory: %m"); |
102 | } | |
5945640e | 103 | |
05eb896f LP |
104 | d = opendir(p); |
105 | if (!d) { | |
106 | /* No such dir? Then no creds where passed. (We conditionalize this on arg_system, since for | |
107 | * the per-service case a non-existing path would indicate an issue since the env var would | |
108 | * be set incorrectly in that case.) */ | |
109 | if (arg_system && errno == ENOENT) | |
110 | goto not_found; | |
111 | ||
112 | return log_error_errno(errno, "Failed to open credentials directory '%s': %m", p); | |
113 | } | |
5945640e | 114 | |
05eb896f | 115 | *ret_dir = d; |
5945640e | 116 | |
05eb896f LP |
117 | if (ret_prefix) |
118 | *ret_prefix = p; | |
5945640e | 119 | |
05eb896f LP |
120 | return 1; |
121 | ||
122 | not_found: | |
123 | *ret_dir = NULL; | |
124 | ||
125 | if (ret_prefix) | |
126 | *ret_prefix = NULL; | |
5945640e | 127 | |
5945640e LP |
128 | return 0; |
129 | } | |
130 | ||
05eb896f | 131 | static int add_credentials_to_table(Table *t, bool encrypted) { |
5d2a48da | 132 | _cleanup_closedir_ DIR *d = NULL; |
05eb896f | 133 | const char *prefix; |
5945640e LP |
134 | int r; |
135 | ||
05eb896f | 136 | assert(t); |
5945640e | 137 | |
07a442ef | 138 | r = open_credential_directory(encrypted, &d, &prefix); |
05eb896f LP |
139 | if (r < 0) |
140 | return r; | |
141 | if (!d) | |
142 | return 0; /* No creds dir set */ | |
5945640e LP |
143 | |
144 | for (;;) { | |
05eb896f | 145 | _cleanup_free_ char *j = NULL; |
5945640e | 146 | const char *secure, *secure_color = NULL; |
254d1313 | 147 | _cleanup_close_ int fd = -EBADF; |
5945640e LP |
148 | struct dirent *de; |
149 | struct stat st; | |
150 | ||
151 | errno = 0; | |
152 | de = readdir_no_dot(d); | |
153 | if (!de) { | |
154 | if (errno == 0) | |
155 | break; | |
156 | ||
157 | return log_error_errno(errno, "Failed to read credentials directory: %m"); | |
158 | } | |
159 | ||
160 | if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN)) | |
161 | continue; | |
162 | ||
163 | if (!credential_name_valid(de->d_name)) | |
164 | continue; | |
165 | ||
166 | fd = openat(dirfd(d), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW); | |
167 | if (fd < 0) { | |
168 | if (errno == ENOENT) /* Vanished by now? */ | |
169 | continue; | |
170 | ||
171 | return log_error_errno(errno, "Failed to open credential '%s': %m", de->d_name); | |
172 | } | |
173 | ||
174 | if (fstat(fd, &st) < 0) | |
175 | return log_error_errno(errno, "Failed to stat credential '%s': %m", de->d_name); | |
176 | ||
177 | if (!S_ISREG(st.st_mode)) | |
178 | continue; | |
179 | ||
05eb896f LP |
180 | if (encrypted) { |
181 | secure = "encrypted"; | |
182 | secure_color = ansi_highlight_green(); | |
183 | } else if ((st.st_mode & 0377) != 0) { | |
5945640e LP |
184 | secure = "insecure"; /* Anything that is accessible more than read-only to its owner is insecure */ |
185 | secure_color = ansi_highlight_red(); | |
186 | } else { | |
187 | r = fd_is_fs_type(fd, RAMFS_MAGIC); | |
188 | if (r < 0) | |
189 | return log_error_errno(r, "Failed to determine backing file system of '%s': %m", de->d_name); | |
190 | ||
79de6eb1 MY |
191 | secure = r > 0 ? "secure" : "weak"; /* ramfs is not swappable, hence "secure", everything else is "weak" */ |
192 | secure_color = r > 0 ? ansi_highlight_green() : ansi_highlight_yellow4(); | |
5945640e LP |
193 | } |
194 | ||
05eb896f LP |
195 | j = path_join(prefix, de->d_name); |
196 | if (!j) | |
197 | return log_oom(); | |
198 | ||
5945640e LP |
199 | r = table_add_many( |
200 | t, | |
201 | TABLE_STRING, de->d_name, | |
202 | TABLE_STRING, secure, | |
203 | TABLE_SET_COLOR, secure_color, | |
05eb896f LP |
204 | TABLE_SIZE, (uint64_t) st.st_size, |
205 | TABLE_STRING, j); | |
5945640e LP |
206 | if (r < 0) |
207 | return table_log_add_error(r); | |
208 | } | |
209 | ||
05eb896f LP |
210 | return 1; /* Creds dir set */ |
211 | } | |
212 | ||
213 | static int verb_list(int argc, char **argv, void *userdata) { | |
214 | _cleanup_(table_unrefp) Table *t = NULL; | |
215 | int r, q; | |
216 | ||
217 | t = table_new("name", "secure", "size", "path"); | |
218 | if (!t) | |
219 | return log_oom(); | |
220 | ||
221 | (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 100); | |
222 | ||
223 | r = add_credentials_to_table(t, /* encrypted= */ true); | |
224 | if (r < 0) | |
225 | return r; | |
226 | ||
227 | q = add_credentials_to_table(t, /* encrypted= */ false); | |
228 | if (q < 0) | |
229 | return q; | |
230 | ||
231 | if (r == 0 && q == 0) { | |
232 | if (arg_system) | |
233 | return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed to system."); | |
234 | ||
235 | return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed. (i.e. $CREDENTIALS_DIRECTORY not set.)"); | |
236 | } | |
237 | ||
2413a0fa | 238 | if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) && table_isempty(t)) { |
5945640e LP |
239 | log_info("No credentials"); |
240 | return 0; | |
241 | } | |
242 | ||
243 | return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); | |
244 | } | |
245 | ||
246 | static int transcode( | |
247 | const void *input, | |
248 | size_t input_size, | |
249 | void **ret_output, | |
250 | size_t *ret_output_size) { | |
251 | ||
252 | int r; | |
253 | ||
254 | assert(input); | |
255 | assert(input_size); | |
256 | assert(ret_output); | |
257 | assert(ret_output_size); | |
258 | ||
259 | switch (arg_transcode) { | |
260 | ||
261 | case TRANSCODE_BASE64: { | |
bc1f27ff | 262 | char *buf; |
5945640e LP |
263 | ssize_t l; |
264 | ||
265 | l = base64mem_full(input, input_size, 79, &buf); | |
266 | if (l < 0) | |
267 | return l; | |
268 | ||
269 | *ret_output = buf; | |
270 | *ret_output_size = l; | |
271 | return 0; | |
272 | } | |
273 | ||
274 | case TRANSCODE_UNBASE64: | |
275 | r = unbase64mem_full(input, input_size, true, ret_output, ret_output_size); | |
276 | if (r == -EPIPE) /* Uneven number of chars */ | |
277 | return -EINVAL; | |
278 | ||
279 | return r; | |
280 | ||
281 | case TRANSCODE_HEX: { | |
282 | char *buf; | |
283 | ||
284 | buf = hexmem(input, input_size); | |
285 | if (!buf) | |
286 | return -ENOMEM; | |
287 | ||
288 | *ret_output = buf; | |
289 | *ret_output_size = input_size * 2; | |
290 | return 0; | |
291 | } | |
292 | ||
293 | case TRANSCODE_UNHEX: | |
294 | r = unhexmem_full(input, input_size, true, ret_output, ret_output_size); | |
295 | if (r == -EPIPE) /* Uneven number of chars */ | |
296 | return -EINVAL; | |
297 | ||
298 | return r; | |
299 | ||
300 | default: | |
04499a70 | 301 | assert_not_reached(); |
5945640e LP |
302 | } |
303 | } | |
304 | ||
305 | static int print_newline(FILE *f, const char *data, size_t l) { | |
306 | int fd; | |
307 | ||
308 | assert(f); | |
309 | assert(data || l == 0); | |
310 | ||
311 | /* If turned off explicitly, don't print newline */ | |
312 | if (arg_newline == 0) | |
313 | return 0; | |
314 | ||
315 | /* If data already has newline, don't print either */ | |
316 | if (l > 0 && data[l-1] == '\n') | |
317 | return 0; | |
318 | ||
319 | /* Don't bother unless this is a tty */ | |
320 | fd = fileno(f); | |
dd9c8da8 | 321 | if (fd >= 0 && !isatty_safe(fd)) |
5945640e LP |
322 | return 0; |
323 | ||
324 | if (fputc('\n', f) != '\n') | |
325 | return log_error_errno(errno, "Failed to write trailing newline: %m"); | |
326 | ||
327 | return 1; | |
328 | } | |
329 | ||
330 | static int write_blob(FILE *f, const void *data, size_t size) { | |
331 | _cleanup_(erase_and_freep) void *transcoded = NULL; | |
332 | int r; | |
333 | ||
334 | if (arg_transcode == TRANSCODE_OFF && | |
335 | arg_json_format_flags != JSON_FORMAT_OFF) { | |
5945640e LP |
336 | _cleanup_(erase_and_freep) char *suffixed = NULL; |
337 | _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; | |
338 | ||
7153213e LP |
339 | r = make_cstring(data, size, MAKE_CSTRING_REFUSE_TRAILING_NUL, &suffixed); |
340 | if (r < 0) | |
341 | return log_error_errno(r, "Unable to convert binary string to C string: %m"); | |
5945640e LP |
342 | |
343 | r = json_parse(suffixed, JSON_PARSE_SENSITIVE, &v, NULL, NULL); | |
344 | if (r < 0) | |
345 | return log_error_errno(r, "Failed to parse JSON: %m"); | |
346 | ||
347 | json_variant_dump(v, arg_json_format_flags, f, NULL); | |
348 | return 0; | |
349 | } | |
350 | ||
351 | if (arg_transcode != TRANSCODE_OFF) { | |
352 | r = transcode(data, size, &transcoded, &size); | |
353 | if (r < 0) | |
354 | return log_error_errno(r, "Failed to transcode data: %m"); | |
355 | ||
356 | data = transcoded; | |
357 | } | |
358 | ||
359 | if (fwrite(data, 1, size, f) != size) | |
513412a6 | 360 | return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write credential data."); |
5945640e LP |
361 | |
362 | r = print_newline(f, data, size); | |
363 | if (r < 0) | |
364 | return r; | |
365 | ||
f9568765 ZJS |
366 | r = fflush_and_check(f); |
367 | if (r < 0) | |
368 | return log_error_errno(r, "Failed to flush output: %m"); | |
5945640e LP |
369 | |
370 | return 0; | |
371 | } | |
372 | ||
373 | static int verb_cat(int argc, char **argv, void *userdata) { | |
05eb896f | 374 | usec_t timestamp; |
5945640e | 375 | int r, ret = 0; |
5945640e | 376 | |
05eb896f | 377 | timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); |
5945640e LP |
378 | |
379 | STRV_FOREACH(cn, strv_skip(argv, 1)) { | |
380 | _cleanup_(erase_and_freep) void *data = NULL; | |
381 | size_t size = 0; | |
05eb896f | 382 | int encrypted; |
5945640e LP |
383 | |
384 | if (!credential_name_valid(*cn)) { | |
385 | log_error("Credential name '%s' is not valid.", *cn); | |
386 | if (ret >= 0) | |
387 | ret = -EINVAL; | |
388 | continue; | |
389 | } | |
390 | ||
05eb896f | 391 | /* Look both in regular and in encrypted credentials */ |
07a442ef | 392 | for (encrypted = 0; encrypted < 2; encrypted++) { |
5d2a48da | 393 | _cleanup_closedir_ DIR *d = NULL; |
05eb896f | 394 | |
07a442ef | 395 | r = open_credential_directory(encrypted, &d, NULL); |
05eb896f LP |
396 | if (r < 0) |
397 | return log_error_errno(r, "Failed to open credentials directory: %m"); | |
398 | if (!d) /* Not set */ | |
399 | continue; | |
400 | ||
401 | r = read_full_file_full( | |
402 | dirfd(d), *cn, | |
403 | UINT64_MAX, SIZE_MAX, | |
404 | READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE, | |
405 | NULL, | |
406 | (char**) &data, &size); | |
407 | if (r == -ENOENT) /* Not found */ | |
408 | continue; | |
409 | if (r >= 0) /* Found */ | |
410 | break; | |
411 | ||
5945640e LP |
412 | log_error_errno(r, "Failed to read credential '%s': %m", *cn); |
413 | if (ret >= 0) | |
414 | ret = r; | |
05eb896f LP |
415 | } |
416 | ||
417 | if (encrypted >= 2) { /* Found nowhere */ | |
418 | log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Credential '%s' not set.", *cn); | |
419 | if (ret >= 0) | |
420 | ret = -ENOENT; | |
421 | ||
5945640e LP |
422 | continue; |
423 | } | |
424 | ||
05eb896f | 425 | if (encrypted) { |
8d042bc4 | 426 | _cleanup_(iovec_done_erase) struct iovec plaintext = {}; |
05eb896f | 427 | |
19f16c99 LP |
428 | if (geteuid() != 0) |
429 | r = ipc_decrypt_credential( | |
430 | *cn, | |
431 | timestamp, | |
432 | uid_is_valid(arg_uid) ? arg_uid : getuid(), | |
433 | &IOVEC_MAKE(data, size), | |
434 | CREDENTIAL_ANY_SCOPE, | |
435 | &plaintext); | |
436 | else | |
437 | r = decrypt_credential_and_warn( | |
438 | *cn, | |
439 | timestamp, | |
440 | arg_tpm2_device, | |
441 | arg_tpm2_signature, | |
442 | uid_is_valid(arg_uid) ? arg_uid : getuid(), | |
443 | &IOVEC_MAKE(data, size), | |
444 | CREDENTIAL_ANY_SCOPE, | |
445 | &plaintext); | |
05eb896f LP |
446 | if (r < 0) |
447 | return r; | |
448 | ||
449 | erase_and_free(data); | |
8d042bc4 LP |
450 | data = TAKE_PTR(plaintext.iov_base); |
451 | size = plaintext.iov_len; | |
05eb896f LP |
452 | } |
453 | ||
5945640e LP |
454 | r = write_blob(stdout, data, size); |
455 | if (r < 0) | |
456 | return r; | |
457 | } | |
458 | ||
459 | return ret; | |
460 | } | |
461 | ||
462 | static int verb_encrypt(int argc, char **argv, void *userdata) { | |
8d042bc4 | 463 | _cleanup_(iovec_done_erase) struct iovec plaintext = {}, output = {}; |
5945640e | 464 | _cleanup_free_ char *base64_buf = NULL, *fname = NULL; |
5945640e | 465 | const char *input_path, *output_path, *name; |
5945640e LP |
466 | ssize_t base64_size; |
467 | usec_t timestamp; | |
468 | int r; | |
469 | ||
470 | assert(argc == 3); | |
471 | ||
2f092762 | 472 | input_path = empty_or_dash(argv[1]) ? NULL : argv[1]; |
5945640e LP |
473 | |
474 | if (input_path) | |
8d042bc4 | 475 | 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, (char**) &plaintext.iov_base, &plaintext.iov_len); |
5945640e | 476 | else |
8d042bc4 | 477 | r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, (char**) &plaintext.iov_base, &plaintext.iov_len); |
5945640e LP |
478 | if (r == -E2BIG) |
479 | return log_error_errno(r, "Plaintext too long for credential (allowed size: %zu).", (size_t) CREDENTIAL_SIZE_MAX); | |
480 | if (r < 0) | |
481 | return log_error_errno(r, "Failed to read plaintext: %m"); | |
482 | ||
2f092762 | 483 | output_path = empty_or_dash(argv[2]) ? NULL : argv[2]; |
5945640e LP |
484 | |
485 | if (arg_name_any) | |
486 | name = NULL; | |
487 | else if (arg_name) | |
488 | name = arg_name; | |
489 | else if (output_path) { | |
490 | r = path_extract_filename(output_path, &fname); | |
491 | if (r < 0) | |
492 | return log_error_errno(r, "Failed to extract filename from '%s': %m", output_path); | |
493 | if (r == O_DIRECTORY) | |
494 | return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", output_path); | |
495 | ||
496 | name = fname; | |
497 | } else { | |
498 | log_warning("No credential name specified, not embedding credential name in encrypted data. (Disable this warning with --name=)"); | |
499 | name = NULL; | |
500 | } | |
501 | ||
502 | timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); | |
503 | ||
504 | if (arg_not_after != USEC_INFINITY && arg_not_after < timestamp) | |
505 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid."); | |
506 | ||
19f16c99 LP |
507 | if (geteuid() != 0) |
508 | r = ipc_encrypt_credential( | |
509 | name, | |
510 | timestamp, | |
511 | arg_not_after, | |
512 | arg_uid, | |
513 | &plaintext, | |
514 | /* flags= */ 0, | |
515 | &output); | |
516 | else | |
517 | r = encrypt_credential_and_warn( | |
518 | arg_with_key, | |
519 | name, | |
520 | timestamp, | |
521 | arg_not_after, | |
522 | arg_tpm2_device, | |
523 | arg_tpm2_pcr_mask, | |
524 | arg_tpm2_public_key, | |
525 | arg_tpm2_public_key_pcr_mask, | |
526 | arg_uid, | |
527 | &plaintext, | |
528 | /* flags= */ 0, | |
529 | &output); | |
5945640e LP |
530 | if (r < 0) |
531 | return r; | |
532 | ||
8d042bc4 | 533 | base64_size = base64mem_full(output.iov_base, output.iov_len, arg_pretty ? 69 : 79, &base64_buf); |
5945640e LP |
534 | if (base64_size < 0) |
535 | return base64_size; | |
536 | ||
c74e13a5 FS |
537 | /* Pretty print makes sense only if we're printing stuff to stdout |
538 | * and if a cred name is provided via --name= (since we can't use | |
539 | * the output file name as the cred name here) */ | |
540 | if (arg_pretty && !output_path && name) { | |
5945640e LP |
541 | _cleanup_free_ char *escaped = NULL, *indented = NULL, *j = NULL; |
542 | ||
c74e13a5 FS |
543 | escaped = cescape(name); |
544 | if (!escaped) | |
545 | return log_oom(); | |
5945640e LP |
546 | |
547 | indented = strreplace(base64_buf, "\n", " \\\n "); | |
548 | if (!indented) | |
549 | return log_oom(); | |
550 | ||
c74e13a5 | 551 | j = strjoin("SetCredentialEncrypted=", escaped, ": \\\n ", indented, "\n"); |
5945640e LP |
552 | if (!j) |
553 | return log_oom(); | |
554 | ||
555 | free_and_replace(base64_buf, j); | |
556 | } | |
557 | ||
558 | if (output_path) | |
559 | r = write_string_file(output_path, base64_buf, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE); | |
560 | else | |
561 | r = write_string_stream(stdout, base64_buf, 0); | |
562 | if (r < 0) | |
563 | return log_error_errno(r, "Failed to write result: %m"); | |
564 | ||
565 | return EXIT_SUCCESS; | |
566 | } | |
567 | ||
568 | static int verb_decrypt(int argc, char **argv, void *userdata) { | |
8d042bc4 LP |
569 | _cleanup_(iovec_done_erase) struct iovec input = {}, plaintext = {}; |
570 | _cleanup_free_ char *fname = NULL; | |
5945640e LP |
571 | _cleanup_fclose_ FILE *output_file = NULL; |
572 | const char *input_path, *output_path, *name; | |
5945640e LP |
573 | usec_t timestamp; |
574 | FILE *f; | |
575 | int r; | |
576 | ||
577 | assert(IN_SET(argc, 2, 3)); | |
578 | ||
2f092762 | 579 | input_path = empty_or_dash(argv[1]) ? NULL : argv[1]; |
5945640e LP |
580 | |
581 | if (input_path) | |
8d042bc4 | 582 | 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, (char**) &input, &input.iov_len); |
5945640e | 583 | else |
8d042bc4 | 584 | r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, (char**) &input, &input.iov_len); |
5945640e LP |
585 | if (r == -E2BIG) |
586 | return log_error_errno(r, "Data too long for encrypted credential (allowed size: %zu).", (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX); | |
587 | if (r < 0) | |
588 | return log_error_errno(r, "Failed to read encrypted credential data: %m"); | |
589 | ||
c6661401 | 590 | output_path = (argc < 3 || empty_or_dash(argv[2])) ? NULL : argv[2]; |
5945640e LP |
591 | |
592 | if (arg_name_any) | |
593 | name = NULL; | |
594 | else if (arg_name) | |
595 | name = arg_name; | |
596 | else if (input_path) { | |
597 | r = path_extract_filename(input_path, &fname); | |
598 | if (r < 0) | |
599 | return log_error_errno(r, "Failed to extract filename from '%s': %m", input_path); | |
600 | if (r == O_DIRECTORY) | |
601 | return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", input_path); | |
602 | ||
603 | name = fname; | |
604 | } else { | |
605 | log_warning("No credential name specified, not validating credential name embedded in encrypted data. (Disable this warning with --name=.)"); | |
606 | name = NULL; | |
607 | } | |
608 | ||
609 | timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); | |
610 | ||
19f16c99 LP |
611 | if (geteuid() != 0) |
612 | r = ipc_decrypt_credential( | |
613 | name, | |
614 | timestamp, | |
615 | arg_uid, | |
616 | &input, | |
617 | /* flags= */ 0, | |
618 | &plaintext); | |
619 | else | |
620 | r = decrypt_credential_and_warn( | |
621 | name, | |
622 | timestamp, | |
623 | arg_tpm2_device, | |
624 | arg_tpm2_signature, | |
625 | arg_uid, | |
626 | &input, | |
aadbe559 | 627 | arg_allow_null ? CREDENTIAL_ALLOW_NULL : 0, |
19f16c99 | 628 | &plaintext); |
5945640e LP |
629 | if (r < 0) |
630 | return r; | |
631 | ||
632 | if (output_path) { | |
633 | output_file = fopen(output_path, "we"); | |
634 | if (!output_file) | |
635 | return log_error_errno(errno, "Failed to create output file '%s': %m", output_path); | |
636 | ||
637 | f = output_file; | |
638 | } else | |
639 | f = stdout; | |
640 | ||
8d042bc4 | 641 | r = write_blob(f, plaintext.iov_base, plaintext.iov_len); |
5945640e LP |
642 | if (r < 0) |
643 | return r; | |
644 | ||
645 | return EXIT_SUCCESS; | |
646 | } | |
647 | ||
648 | static int verb_setup(int argc, char **argv, void *userdata) { | |
8d042bc4 | 649 | _cleanup_(iovec_done_erase) struct iovec host_key = {}; |
5945640e LP |
650 | int r; |
651 | ||
8d042bc4 | 652 | r = get_credential_host_secret(CREDENTIAL_SECRET_GENERATE|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED, &host_key); |
5945640e LP |
653 | if (r < 0) |
654 | return log_error_errno(r, "Failed to setup credentials host key: %m"); | |
655 | ||
8d042bc4 | 656 | log_info("%zu byte credentials host key set up.", host_key.iov_len); |
5945640e LP |
657 | |
658 | return EXIT_SUCCESS; | |
659 | } | |
660 | ||
6e0cb815 LP |
661 | static int verb_has_tpm2(int argc, char **argv, void *userdata) { |
662 | Tpm2Support s; | |
663 | ||
664 | s = tpm2_support(); | |
665 | ||
666 | if (!arg_quiet) { | |
667 | if (s == TPM2_SUPPORT_FULL) | |
668 | puts("yes"); | |
669 | else if (s == TPM2_SUPPORT_NONE) | |
670 | puts("no"); | |
671 | else | |
672 | puts("partial"); | |
673 | ||
674 | printf("%sfirmware\n" | |
675 | "%sdriver\n" | |
300bba79 | 676 | "%ssystem\n" |
33931049 DDM |
677 | "%ssubsystem\n" |
678 | "%slibraries\n", | |
6e0cb815 LP |
679 | plus_minus(s & TPM2_SUPPORT_FIRMWARE), |
680 | plus_minus(s & TPM2_SUPPORT_DRIVER), | |
300bba79 | 681 | plus_minus(s & TPM2_SUPPORT_SYSTEM), |
33931049 DDM |
682 | plus_minus(s & TPM2_SUPPORT_SUBSYSTEM), |
683 | plus_minus(s & TPM2_SUPPORT_LIBRARIES)); | |
6e0cb815 LP |
684 | } |
685 | ||
686 | /* Return inverted bit flags. So that TPM2_SUPPORT_FULL becomes EXIT_SUCCESS and the other values | |
687 | * become some reasonable values 1…7. i.e. the flags we return here tell what is missing rather than | |
8ac6b05b | 688 | * what is there, acknowledging the fact that for process exit statuses it is customary to return |
6e0cb815 LP |
689 | * zero (EXIT_FAILURE) when all is good, instead of all being bad. */ |
690 | return ~s & TPM2_SUPPORT_FULL; | |
691 | } | |
692 | ||
5945640e LP |
693 | static int verb_help(int argc, char **argv, void *userdata) { |
694 | _cleanup_free_ char *link = NULL; | |
695 | int r; | |
696 | ||
697 | r = terminal_urlify_man("systemd-creds", "1", &link); | |
698 | if (r < 0) | |
699 | return log_oom(); | |
700 | ||
701 | printf("%1$s [OPTIONS...] COMMAND ...\n" | |
702 | "\n%5$sDisplay and Process Credentials.%6$s\n" | |
703 | "\n%3$sCommands:%4$s\n" | |
704 | " list Show installed and available versions\n" | |
705 | " cat CREDENTIAL... Show specified credentials\n" | |
706 | " setup Generate credentials host key, if not existing yet\n" | |
707 | " encrypt INPUT OUTPUT Encrypt plaintext credential file and write to\n" | |
708 | " ciphertext credential file\n" | |
709 | " decrypt INPUT [OUTPUT] Decrypt ciphertext credential file and write to\n" | |
710 | " plaintext credential file\n" | |
6e0cb815 | 711 | " has-tpm2 Report whether TPM2 support is available\n" |
5945640e LP |
712 | " -h --help Show this help\n" |
713 | " --version Show package version\n" | |
714 | "\n%3$sOptions:%4$s\n" | |
715 | " --no-pager Do not pipe output into a pager\n" | |
716 | " --no-legend Do not show the headers and footers\n" | |
717 | " --json=pretty|short|off\n" | |
718 | " Generate JSON output\n" | |
719 | " --system Show credentials passed to system\n" | |
720 | " --transcode=base64|unbase64|hex|unhex\n" | |
721 | " Transcode credential data\n" | |
722 | " --newline=auto|yes|no\n" | |
723 | " Suffix output with newline\n" | |
724 | " -p --pretty Output as SetCredentialEncrypted= line\n" | |
725 | " --name=NAME Override filename included in encrypted credential\n" | |
726 | " --timestamp=TIME Include specified timestamp in encrypted credential\n" | |
727 | " --not-after=TIME Include specified invalidation time in encrypted\n" | |
728 | " credential\n" | |
bfbd7ac9 | 729 | " --with-key=host|tpm2|host+tpm2|null|auto|auto-initrd\n" |
5945640e LP |
730 | " Which keys to encrypt with\n" |
731 | " -H Shortcut for --with-key=host\n" | |
732 | " -T Shortcut for --with-key=tpm2\n" | |
733 | " --tpm2-device=PATH\n" | |
734 | " Pick TPM2 device\n" | |
735 | " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n" | |
75ddec93 LP |
736 | " Specify TPM2 PCRs to seal against (fixed hash)\n" |
737 | " --tpm2-public-key=PATH\n" | |
738 | " Specify PEM certificate to seal against\n" | |
739 | " --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n" | |
740 | " Specify TPM2 PCRs to seal against (public key)\n" | |
741 | " --tpm2-signature=PATH\n" | |
742 | " Specify signature for public key PCR policy\n" | |
c85b68f6 LP |
743 | " --user Select user-scoped credential encryption\n" |
744 | " --uid=UID Select user for scoped credentials\n" | |
aadbe559 | 745 | " --allow-null Allow decrypting credentials with empty key\n" |
6e0cb815 | 746 | " -q --quiet Suppress output for 'has-tpm2' verb\n" |
65064e2f AAF |
747 | "\nSee the %2$s for details.\n", |
748 | program_invocation_short_name, | |
749 | link, | |
750 | ansi_underline(), | |
751 | ansi_normal(), | |
752 | ansi_highlight(), | |
753 | ansi_normal()); | |
5945640e LP |
754 | |
755 | return 0; | |
756 | } | |
757 | ||
758 | static int parse_argv(int argc, char *argv[]) { | |
759 | ||
760 | enum { | |
761 | ARG_VERSION = 0x100, | |
762 | ARG_NO_PAGER, | |
763 | ARG_NO_LEGEND, | |
764 | ARG_JSON, | |
765 | ARG_SYSTEM, | |
766 | ARG_TRANSCODE, | |
767 | ARG_NEWLINE, | |
768 | ARG_WITH_KEY, | |
769 | ARG_TPM2_DEVICE, | |
770 | ARG_TPM2_PCRS, | |
75ddec93 LP |
771 | ARG_TPM2_PUBLIC_KEY, |
772 | ARG_TPM2_PUBLIC_KEY_PCRS, | |
773 | ARG_TPM2_SIGNATURE, | |
5945640e LP |
774 | ARG_NAME, |
775 | ARG_TIMESTAMP, | |
776 | ARG_NOT_AFTER, | |
c85b68f6 LP |
777 | ARG_USER, |
778 | ARG_UID, | |
aadbe559 | 779 | ARG_ALLOW_NULL, |
5945640e LP |
780 | }; |
781 | ||
782 | static const struct option options[] = { | |
75ddec93 LP |
783 | { "help", no_argument, NULL, 'h' }, |
784 | { "version", no_argument, NULL, ARG_VERSION }, | |
785 | { "no-pager", no_argument, NULL, ARG_NO_PAGER }, | |
786 | { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, | |
787 | { "json", required_argument, NULL, ARG_JSON }, | |
788 | { "system", no_argument, NULL, ARG_SYSTEM }, | |
789 | { "transcode", required_argument, NULL, ARG_TRANSCODE }, | |
790 | { "newline", required_argument, NULL, ARG_NEWLINE }, | |
791 | { "pretty", no_argument, NULL, 'p' }, | |
792 | { "with-key", required_argument, NULL, ARG_WITH_KEY }, | |
793 | { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, | |
794 | { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, | |
795 | { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY }, | |
796 | { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS }, | |
797 | { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE }, | |
798 | { "name", required_argument, NULL, ARG_NAME }, | |
799 | { "timestamp", required_argument, NULL, ARG_TIMESTAMP }, | |
800 | { "not-after", required_argument, NULL, ARG_NOT_AFTER }, | |
801 | { "quiet", no_argument, NULL, 'q' }, | |
c85b68f6 LP |
802 | { "user", no_argument, NULL, ARG_USER }, |
803 | { "uid", required_argument, NULL, ARG_UID }, | |
aadbe559 | 804 | { "allow-null", no_argument, NULL, ARG_ALLOW_NULL }, |
5945640e LP |
805 | {} |
806 | }; | |
807 | ||
808 | int c, r; | |
809 | ||
810 | assert(argc >= 0); | |
811 | assert(argv); | |
812 | ||
6e0cb815 | 813 | while ((c = getopt_long(argc, argv, "hHTpq", options, NULL)) >= 0) { |
5945640e LP |
814 | |
815 | switch (c) { | |
816 | ||
817 | case 'h': | |
818 | return verb_help(0, NULL, NULL); | |
819 | ||
820 | case ARG_VERSION: | |
821 | return version(); | |
822 | ||
823 | case ARG_NO_PAGER: | |
824 | arg_pager_flags |= PAGER_DISABLE; | |
825 | break; | |
826 | ||
827 | case ARG_NO_LEGEND: | |
828 | arg_legend = false; | |
829 | break; | |
830 | ||
831 | case ARG_JSON: | |
832 | r = parse_json_argument(optarg, &arg_json_format_flags); | |
833 | if (r <= 0) | |
834 | return r; | |
835 | ||
836 | break; | |
837 | ||
838 | case ARG_SYSTEM: | |
839 | arg_system = true; | |
840 | break; | |
841 | ||
842 | case ARG_TRANSCODE: | |
843 | if (parse_boolean(optarg) == 0) /* If specified as "false", turn transcoding off */ | |
844 | arg_transcode = TRANSCODE_OFF; | |
845 | else { | |
846 | TranscodeMode m; | |
847 | ||
848 | m = transcode_mode_from_string(optarg); | |
849 | if (m < 0) | |
850 | return log_error_errno(m, "Failed to parse transcode mode: %m"); | |
851 | ||
852 | arg_transcode = m; | |
853 | } | |
854 | ||
855 | break; | |
856 | ||
857 | case ARG_NEWLINE: | |
858 | if (isempty(optarg) || streq(optarg, "auto")) | |
859 | arg_newline = -1; | |
860 | else { | |
51214cf4 | 861 | r = parse_boolean_argument("--newline=", optarg, NULL); |
5945640e LP |
862 | if (r < 0) |
863 | return r; | |
864 | ||
51214cf4 | 865 | arg_newline = r; |
5945640e LP |
866 | } |
867 | break; | |
868 | ||
869 | case 'p': | |
870 | arg_pretty = true; | |
871 | break; | |
872 | ||
873 | case ARG_WITH_KEY: | |
874 | if (isempty(optarg) || streq(optarg, "auto")) | |
571d829e | 875 | arg_with_key = _CRED_AUTO; |
b6553329 LP |
876 | else if (streq(optarg, "auto-initrd")) |
877 | arg_with_key = _CRED_AUTO_INITRD; | |
5945640e LP |
878 | else if (streq(optarg, "host")) |
879 | arg_with_key = CRED_AES256_GCM_BY_HOST; | |
880 | else if (streq(optarg, "tpm2")) | |
881 | arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC; | |
75ddec93 LP |
882 | else if (streq(optarg, "tpm2-with-public-key")) |
883 | arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK; | |
5945640e LP |
884 | else if (STR_IN_SET(optarg, "host+tpm2", "tpm2+host")) |
885 | arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC; | |
75ddec93 LP |
886 | else if (STR_IN_SET(optarg, "host+tpm2-with-public-key", "tpm2-with-public-key+host")) |
887 | arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK; | |
6d78dc28 LP |
888 | else if (STR_IN_SET(optarg, "null", "tpm2-absent")) |
889 | arg_with_key = CRED_AES256_GCM_BY_NULL; | |
5945640e LP |
890 | else |
891 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown key type: %s", optarg); | |
892 | ||
893 | break; | |
894 | ||
895 | case 'H': | |
896 | arg_with_key = CRED_AES256_GCM_BY_HOST; | |
897 | break; | |
898 | ||
899 | case 'T': | |
900 | arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC; | |
901 | break; | |
902 | ||
d1829af9 | 903 | case ARG_TPM2_DEVICE: |
5945640e LP |
904 | if (streq(optarg, "list")) |
905 | return tpm2_list_devices(); | |
906 | ||
d1829af9 | 907 | arg_tpm2_device = streq(optarg, "auto") ? NULL : optarg; |
5945640e | 908 | break; |
5945640e | 909 | |
75ddec93 | 910 | case ARG_TPM2_PCRS: /* For fixed hash PCR policies only */ |
07c04061 | 911 | r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_tpm2_pcr_mask); |
5945640e LP |
912 | if (r < 0) |
913 | return r; | |
914 | ||
5945640e | 915 | break; |
5945640e | 916 | |
75ddec93 LP |
917 | case ARG_TPM2_PUBLIC_KEY: |
918 | r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key); | |
919 | if (r < 0) | |
920 | return r; | |
921 | ||
922 | break; | |
923 | ||
924 | case ARG_TPM2_PUBLIC_KEY_PCRS: /* For public key PCR policies only */ | |
07c04061 | 925 | r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_tpm2_public_key_pcr_mask); |
75ddec93 LP |
926 | if (r < 0) |
927 | return r; | |
928 | ||
929 | break; | |
930 | ||
931 | case ARG_TPM2_SIGNATURE: | |
932 | r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature); | |
933 | if (r < 0) | |
934 | return r; | |
935 | ||
936 | break; | |
937 | ||
5945640e LP |
938 | case ARG_NAME: |
939 | if (isempty(optarg)) { | |
940 | arg_name = NULL; | |
941 | arg_name_any = true; | |
942 | break; | |
943 | } | |
944 | ||
945 | if (!credential_name_valid(optarg)) | |
946 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", optarg); | |
947 | ||
948 | arg_name = optarg; | |
949 | arg_name_any = false; | |
950 | break; | |
951 | ||
952 | case ARG_TIMESTAMP: | |
953 | r = parse_timestamp(optarg, &arg_timestamp); | |
954 | if (r < 0) | |
955 | return log_error_errno(r, "Failed to parse timestamp: %s", optarg); | |
956 | ||
957 | break; | |
958 | ||
959 | case ARG_NOT_AFTER: | |
960 | r = parse_timestamp(optarg, &arg_not_after); | |
961 | if (r < 0) | |
962 | return log_error_errno(r, "Failed to parse --not-after= timestamp: %s", optarg); | |
963 | ||
964 | break; | |
965 | ||
c85b68f6 LP |
966 | case ARG_USER: |
967 | if (!uid_is_valid(arg_uid)) | |
968 | arg_uid = getuid(); | |
969 | ||
970 | break; | |
971 | ||
972 | case ARG_UID: | |
973 | if (isempty(optarg)) | |
974 | arg_uid = UID_INVALID; | |
975 | else if (streq(optarg, "self")) | |
976 | arg_uid = getuid(); | |
977 | else { | |
978 | const char *name = optarg; | |
979 | ||
980 | r = get_user_creds( | |
981 | &name, | |
982 | &arg_uid, | |
983 | /* ret_gid= */ NULL, | |
984 | /* ret_home= */ NULL, | |
985 | /* ret_shell= */ NULL, | |
986 | /* flags= */ 0); | |
987 | if (r < 0) | |
988 | return log_error_errno(r, "Failed to resolve user '%s': %m", optarg); | |
989 | } | |
990 | break; | |
991 | ||
aadbe559 LN |
992 | case ARG_ALLOW_NULL: |
993 | arg_allow_null = true; | |
994 | break; | |
995 | ||
6e0cb815 LP |
996 | case 'q': |
997 | arg_quiet = true; | |
998 | break; | |
999 | ||
5945640e LP |
1000 | case '?': |
1001 | return -EINVAL; | |
1002 | ||
1003 | default: | |
04499a70 | 1004 | assert_not_reached(); |
5945640e LP |
1005 | } |
1006 | } | |
1007 | ||
c85b68f6 LP |
1008 | if (uid_is_valid(arg_uid)) { |
1009 | /* If a UID is specified, then switch to scoped credentials */ | |
1010 | ||
1011 | if (sd_id128_equal(arg_with_key, _CRED_AUTO)) | |
1012 | arg_with_key = _CRED_AUTO_SCOPED; | |
1013 | else if (sd_id128_in_set(arg_with_key, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_SCOPED)) | |
1014 | arg_with_key = CRED_AES256_GCM_BY_HOST_SCOPED; | |
1015 | else if (sd_id128_in_set(arg_with_key, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED)) | |
1016 | arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED; | |
1017 | else if (sd_id128_in_set(arg_with_key, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) | |
1018 | arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED; | |
1019 | else | |
1020 | return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected key not available in --uid= scoped mode, refusing."); | |
1021 | } | |
1022 | ||
5945640e LP |
1023 | if (arg_tpm2_pcr_mask == UINT32_MAX) |
1024 | arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; | |
75ddec93 | 1025 | if (arg_tpm2_public_key_pcr_mask == UINT32_MAX) |
2099cd62 | 1026 | arg_tpm2_public_key_pcr_mask = UINT32_C(1) << TPM2_PCR_KERNEL_BOOT; |
5945640e | 1027 | |
644f19c7 LP |
1028 | r = varlink_invocation(VARLINK_ALLOW_ACCEPT); |
1029 | if (r < 0) | |
1030 | return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); | |
1031 | arg_varlink = r; | |
1032 | ||
5945640e LP |
1033 | return 1; |
1034 | } | |
1035 | ||
1036 | static int creds_main(int argc, char *argv[]) { | |
1037 | ||
1038 | static const Verb verbs[] = { | |
6e0cb815 LP |
1039 | { "list", VERB_ANY, 1, VERB_DEFAULT, verb_list }, |
1040 | { "cat", 2, VERB_ANY, 0, verb_cat }, | |
1041 | { "encrypt", 3, 3, 0, verb_encrypt }, | |
1042 | { "decrypt", 2, 3, 0, verb_decrypt }, | |
1043 | { "setup", VERB_ANY, 1, 0, verb_setup }, | |
1044 | { "help", VERB_ANY, 1, 0, verb_help }, | |
1045 | { "has-tpm2", VERB_ANY, 1, 0, verb_has_tpm2 }, | |
5945640e LP |
1046 | {} |
1047 | }; | |
1048 | ||
1049 | return dispatch_verb(argc, argv, verbs, NULL); | |
1050 | } | |
1051 | ||
8464f7cb LP |
1052 | #define TIMESTAMP_FRESH_MAX (30*USEC_PER_SEC) |
1053 | ||
1054 | static bool timestamp_is_fresh(usec_t x) { | |
1055 | usec_t n = now(CLOCK_REALTIME); | |
1056 | ||
1057 | /* We'll only allow unprivileged encryption/decryption for somehwhat "fresh" timestamps */ | |
1058 | ||
1059 | if (x > n) | |
1060 | return x - n <= TIMESTAMP_FRESH_MAX; | |
1061 | else | |
1062 | return n - x <= TIMESTAMP_FRESH_MAX; | |
1063 | } | |
1064 | ||
1065 | typedef enum CredentialScope { | |
1066 | CREDENTIAL_SYSTEM, | |
1067 | CREDENTIAL_USER, | |
1068 | /* One day we should add more here, for example, per-app/per-service credentials */ | |
1069 | _CREDENTIAL_SCOPE_MAX, | |
1070 | _CREDENTIAL_SCOPE_INVALID = -EINVAL, | |
1071 | } CredentialScope; | |
1072 | ||
1073 | static const char* credential_scope_table[_CREDENTIAL_SCOPE_MAX] = { | |
1074 | [CREDENTIAL_SYSTEM] = "system", | |
1075 | [CREDENTIAL_USER] = "user", | |
1076 | }; | |
1077 | ||
1078 | DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(credential_scope, CredentialScope); | |
1079 | static JSON_DISPATCH_ENUM_DEFINE(dispatch_credential_scope, CredentialScope, credential_scope_from_string); | |
1080 | ||
644f19c7 LP |
1081 | typedef struct MethodEncryptParameters { |
1082 | const char *name; | |
1083 | const char *text; | |
1084 | struct iovec data; | |
1085 | uint64_t timestamp; | |
1086 | uint64_t not_after; | |
8464f7cb LP |
1087 | CredentialScope scope; |
1088 | uid_t uid; | |
644f19c7 LP |
1089 | } MethodEncryptParameters; |
1090 | ||
1091 | static void method_encrypt_parameters_done(MethodEncryptParameters *p) { | |
1092 | assert(p); | |
1093 | ||
1094 | iovec_done_erase(&p->data); | |
1095 | } | |
1096 | ||
8464f7cb LP |
1097 | static int settle_scope( |
1098 | Varlink *link, | |
1099 | CredentialScope *scope, | |
1100 | uid_t *uid, | |
1101 | CredentialFlags *flags, | |
1102 | bool *any_scope_after_polkit) { | |
1103 | ||
1104 | uid_t peer_uid; | |
1105 | int r; | |
1106 | ||
1107 | assert(link); | |
1108 | assert(scope); | |
1109 | assert(uid); | |
1110 | assert(flags); | |
1111 | ||
1112 | r = varlink_get_peer_uid(link, &peer_uid); | |
1113 | if (r < 0) | |
1114 | return r; | |
1115 | ||
1116 | if (*scope < 0) { | |
1117 | if (uid_is_valid(*uid)) | |
1118 | *scope = CREDENTIAL_USER; | |
1119 | else { | |
1120 | *scope = CREDENTIAL_SYSTEM; /* When encrypting, we spit out a system credential */ | |
1121 | *uid = peer_uid; /* When decrypting a user credential, use this UID */ | |
1122 | } | |
1123 | ||
1124 | if (peer_uid == 0) | |
1125 | *flags |= CREDENTIAL_ANY_SCOPE; | |
1126 | ||
1127 | if (any_scope_after_polkit) | |
1128 | *any_scope_after_polkit = true; | |
1129 | } else if (*scope == CREDENTIAL_USER) { | |
1130 | if (!uid_is_valid(*uid)) | |
1131 | *uid = peer_uid; | |
1132 | } else { | |
1133 | assert(*scope == CREDENTIAL_SYSTEM); | |
1134 | if (uid_is_valid(*uid)) | |
1135 | return varlink_error_invalid_parameter_name(link, "uid"); | |
1136 | } | |
1137 | ||
1138 | return 0; | |
1139 | } | |
1140 | ||
644f19c7 LP |
1141 | static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { |
1142 | ||
1143 | static const JsonDispatch dispatch_table[] = { | |
1144 | { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodEncryptParameters, name), 0 }, | |
1145 | { "text", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodEncryptParameters, text), 0 }, | |
1146 | { "data", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodEncryptParameters, data), 0 }, | |
1147 | { "timestamp", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodEncryptParameters, timestamp), 0 }, | |
1148 | { "notAfter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodEncryptParameters, not_after), 0 }, | |
8464f7cb LP |
1149 | { "scope", JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodEncryptParameters, scope), 0 }, |
1150 | { "uid", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uid_gid, offsetof(MethodEncryptParameters, uid), 0 }, | |
caef0bc3 | 1151 | VARLINK_DISPATCH_POLKIT_FIELD, |
644f19c7 LP |
1152 | {} |
1153 | }; | |
1154 | _cleanup_(method_encrypt_parameters_done) MethodEncryptParameters p = { | |
1155 | .timestamp = UINT64_MAX, | |
1156 | .not_after = UINT64_MAX, | |
8464f7cb LP |
1157 | .scope = _CREDENTIAL_SCOPE_INVALID, |
1158 | .uid = UID_INVALID, | |
644f19c7 LP |
1159 | }; |
1160 | _cleanup_(iovec_done) struct iovec output = {}; | |
caef0bc3 | 1161 | Hashmap **polkit_registry = ASSERT_PTR(userdata); |
8464f7cb LP |
1162 | CredentialFlags cflags = 0; |
1163 | bool timestamp_fresh; | |
1164 | uid_t peer_uid; | |
644f19c7 LP |
1165 | int r; |
1166 | ||
1167 | assert(link); | |
1168 | ||
644f19c7 LP |
1169 | r = varlink_dispatch(link, parameters, dispatch_table, &p); |
1170 | if (r != 0) | |
1171 | return r; | |
1172 | ||
1173 | if (p.name && !credential_name_valid(p.name)) | |
1174 | return varlink_error_invalid_parameter_name(link, "name"); | |
1175 | /* Specifying both or neither the text string and the binary data is not allowed */ | |
1176 | if (!!p.text == !!p.data.iov_base) | |
1177 | return varlink_error_invalid_parameter_name(link, "data"); | |
8464f7cb | 1178 | if (p.timestamp == UINT64_MAX) { |
644f19c7 | 1179 | p.timestamp = now(CLOCK_REALTIME); |
8464f7cb LP |
1180 | timestamp_fresh = true; |
1181 | } else | |
1182 | timestamp_fresh = timestamp_is_fresh(p.timestamp); | |
644f19c7 LP |
1183 | if (p.not_after != UINT64_MAX && p.not_after < p.timestamp) |
1184 | return varlink_error_invalid_parameter_name(link, "notAfter"); | |
1185 | ||
8464f7cb LP |
1186 | r = settle_scope(link, &p.scope, &p.uid, &cflags, /* any_scope_after_polkit= */ NULL); |
1187 | if (r < 0) | |
1188 | return r; | |
1189 | ||
1190 | r = varlink_get_peer_uid(link, &peer_uid); | |
1191 | if (r < 0) | |
caef0bc3 LP |
1192 | return r; |
1193 | ||
8464f7cb LP |
1194 | /* Relax security requirements if peer wants to encrypt credentials for themselves */ |
1195 | bool own_scope = p.scope == CREDENTIAL_USER && p.uid == peer_uid; | |
1196 | ||
1197 | if (!own_scope || !timestamp_fresh) { | |
9d7f6b3d | 1198 | /* Insist on PK if client wants to encrypt for another user or the system, or if the timestamp was explicitly overridden. */ |
8464f7cb LP |
1199 | r = varlink_verify_polkit_async( |
1200 | link, | |
1201 | /* bus= */ NULL, | |
1202 | "io.systemd.credentials.encrypt", | |
1203 | /* details= */ NULL, | |
8464f7cb LP |
1204 | polkit_registry); |
1205 | if (r <= 0) | |
1206 | return r; | |
1207 | } | |
1208 | ||
644f19c7 | 1209 | r = encrypt_credential_and_warn( |
8464f7cb | 1210 | p.scope == CREDENTIAL_USER ? _CRED_AUTO_SCOPED : _CRED_AUTO, |
644f19c7 LP |
1211 | p.name, |
1212 | p.timestamp, | |
1213 | p.not_after, | |
1214 | arg_tpm2_device, | |
1215 | arg_tpm2_pcr_mask, | |
1216 | arg_tpm2_public_key, | |
1217 | arg_tpm2_public_key_pcr_mask, | |
8464f7cb | 1218 | p.uid, |
8d042bc4 | 1219 | p.text ? &IOVEC_MAKE_STRING(p.text) : &p.data, |
8464f7cb | 1220 | cflags, |
8d042bc4 | 1221 | &output); |
8464f7cb LP |
1222 | if (r == -ESRCH) |
1223 | return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); | |
644f19c7 LP |
1224 | if (r < 0) |
1225 | return r; | |
1226 | ||
1227 | _cleanup_(json_variant_unrefp) JsonVariant *reply = NULL; | |
1228 | ||
1229 | r = json_build(&reply, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_IOVEC_BASE64("blob", &output))); | |
1230 | if (r < 0) | |
1231 | return r; | |
1232 | ||
1233 | /* Let's also mark the (theoretically encrypted) reply as sensitive, in case the NULL encryption scheme was used. */ | |
1234 | json_variant_sensitive(reply); | |
1235 | ||
1236 | return varlink_reply(link, reply); | |
1237 | } | |
1238 | ||
1239 | typedef struct MethodDecryptParameters { | |
1240 | const char *name; | |
1241 | struct iovec blob; | |
1242 | uint64_t timestamp; | |
8464f7cb LP |
1243 | CredentialScope scope; |
1244 | uid_t uid; | |
644f19c7 LP |
1245 | } MethodDecryptParameters; |
1246 | ||
1247 | static void method_decrypt_parameters_done(MethodDecryptParameters *p) { | |
1248 | assert(p); | |
1249 | ||
1250 | iovec_done_erase(&p->blob); | |
1251 | } | |
1252 | ||
1253 | static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { | |
1254 | ||
1255 | static const JsonDispatch dispatch_table[] = { | |
caef0bc3 LP |
1256 | { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodDecryptParameters, name), 0 }, |
1257 | { "blob", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodDecryptParameters, blob), JSON_MANDATORY }, | |
1258 | { "timestamp", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodDecryptParameters, timestamp), 0 }, | |
8464f7cb LP |
1259 | { "scope", JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodDecryptParameters, scope), 0 }, |
1260 | { "uid", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uid_gid, offsetof(MethodDecryptParameters, uid), 0 }, | |
caef0bc3 | 1261 | VARLINK_DISPATCH_POLKIT_FIELD, |
644f19c7 LP |
1262 | {} |
1263 | }; | |
1264 | _cleanup_(method_decrypt_parameters_done) MethodDecryptParameters p = { | |
1265 | .timestamp = UINT64_MAX, | |
8464f7cb LP |
1266 | .scope = _CREDENTIAL_SCOPE_INVALID, |
1267 | .uid = UID_INVALID, | |
644f19c7 | 1268 | }; |
8464f7cb | 1269 | bool timestamp_fresh, any_scope_after_polkit = false; |
644f19c7 | 1270 | _cleanup_(iovec_done_erase) struct iovec output = {}; |
caef0bc3 | 1271 | Hashmap **polkit_registry = ASSERT_PTR(userdata); |
8464f7cb LP |
1272 | CredentialFlags cflags = 0; |
1273 | uid_t peer_uid; | |
644f19c7 LP |
1274 | int r; |
1275 | ||
1276 | assert(link); | |
1277 | ||
644f19c7 LP |
1278 | r = varlink_dispatch(link, parameters, dispatch_table, &p); |
1279 | if (r != 0) | |
1280 | return r; | |
1281 | ||
1282 | if (p.name && !credential_name_valid(p.name)) | |
1283 | return varlink_error_invalid_parameter_name(link, "name"); | |
8464f7cb | 1284 | if (p.timestamp == UINT64_MAX) { |
644f19c7 | 1285 | p.timestamp = now(CLOCK_REALTIME); |
8464f7cb LP |
1286 | timestamp_fresh = true; |
1287 | } else | |
1288 | timestamp_fresh = timestamp_is_fresh(p.timestamp); | |
644f19c7 | 1289 | |
8464f7cb LP |
1290 | r = settle_scope(link, &p.scope, &p.uid, &cflags, &any_scope_after_polkit); |
1291 | if (r < 0) | |
caef0bc3 LP |
1292 | return r; |
1293 | ||
8464f7cb LP |
1294 | r = varlink_get_peer_uid(link, &peer_uid); |
1295 | if (r < 0) | |
1296 | return r; | |
1297 | ||
1298 | /* Relax security requirements if peer wants to encrypt credentials for themselves */ | |
1299 | bool own_scope = p.scope == CREDENTIAL_USER && p.uid == peer_uid; | |
1300 | bool ask_polkit = !own_scope || !timestamp_fresh; | |
1301 | for (;;) { | |
1302 | if (ask_polkit) { | |
1303 | r = varlink_verify_polkit_async( | |
1304 | link, | |
1305 | /* bus= */ NULL, | |
1306 | "io.systemd.credentials.decrypt", | |
1307 | /* details= */ NULL, | |
8464f7cb LP |
1308 | polkit_registry); |
1309 | if (r <= 0) | |
1310 | return r; | |
1311 | ||
1312 | /* Now that we have authenticated, it's fine to allow unpriv clients access to system secrets */ | |
1313 | if (any_scope_after_polkit) | |
1314 | cflags |= CREDENTIAL_ANY_SCOPE; | |
1315 | } | |
1316 | ||
1317 | r = decrypt_credential_and_warn( | |
1318 | p.name, | |
1319 | p.timestamp, | |
1320 | arg_tpm2_device, | |
1321 | arg_tpm2_signature, | |
1322 | p.uid, | |
1323 | &p.blob, | |
1324 | cflags, | |
1325 | &output); | |
1326 | if (r != -EMEDIUMTYPE || ask_polkit || !any_scope_after_polkit) | |
1327 | break; | |
1328 | ||
1329 | /* So the secret was apparently intended for the system. Let's retry decrypting it after | |
1330 | * acquiring polkit's permission. */ | |
1331 | ask_polkit = true; | |
1332 | } | |
1333 | ||
644f19c7 LP |
1334 | if (r == -EBADMSG) |
1335 | return varlink_error(link, "io.systemd.Credentials.BadFormat", NULL); | |
1336 | if (r == -EREMOTE) | |
1337 | return varlink_error(link, "io.systemd.Credentials.NameMismatch", NULL); | |
1338 | if (r == -ESTALE) | |
1339 | return varlink_error(link, "io.systemd.Credentials.TimeMismatch", NULL); | |
8464f7cb LP |
1340 | if (r == -ESRCH) |
1341 | return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); | |
1342 | if (r == -EMEDIUMTYPE) | |
1343 | return varlink_error(link, "io.systemd.Credentials.BadScope", NULL); | |
644f19c7 LP |
1344 | if (r < 0) |
1345 | return r; | |
1346 | ||
1347 | _cleanup_(json_variant_unrefp) JsonVariant *reply = NULL; | |
1348 | ||
1349 | r = json_build(&reply, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_IOVEC_BASE64("data", &output))); | |
1350 | if (r < 0) | |
1351 | return r; | |
1352 | ||
1353 | json_variant_sensitive(reply); | |
1354 | ||
1355 | return varlink_reply(link, reply); | |
1356 | } | |
1357 | ||
5945640e LP |
1358 | static int run(int argc, char *argv[]) { |
1359 | int r; | |
1360 | ||
1361 | log_setup(); | |
1362 | ||
1363 | r = parse_argv(argc, argv); | |
1364 | if (r <= 0) | |
1365 | return r; | |
1366 | ||
644f19c7 LP |
1367 | if (arg_varlink) { |
1368 | _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL; | |
caef0bc3 | 1369 | _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL; |
644f19c7 LP |
1370 | |
1371 | /* Invocation as Varlink service */ | |
1372 | ||
a570877c | 1373 | r = varlink_server_new(&varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA|VARLINK_SERVER_INPUT_SENSITIVE); |
644f19c7 LP |
1374 | if (r < 0) |
1375 | return log_error_errno(r, "Failed to allocate Varlink server: %m"); | |
1376 | ||
1377 | r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_Credentials); | |
1378 | if (r < 0) | |
1379 | return log_error_errno(r, "Failed to add Varlink interface: %m"); | |
1380 | ||
1381 | r = varlink_server_bind_method_many( | |
1382 | varlink_server, | |
1383 | "io.systemd.Credentials.Encrypt", vl_method_encrypt, | |
1384 | "io.systemd.Credentials.Decrypt", vl_method_decrypt); | |
1385 | if (r < 0) | |
1386 | return log_error_errno(r, "Failed to bind Varlink methods: %m"); | |
1387 | ||
caef0bc3 LP |
1388 | varlink_server_set_userdata(varlink_server, &polkit_registry); |
1389 | ||
644f19c7 LP |
1390 | r = varlink_server_loop_auto(varlink_server); |
1391 | if (r < 0) | |
1392 | return log_error_errno(r, "Failed to run Varlink event loop: %m"); | |
1393 | ||
1394 | return 0; | |
1395 | } | |
1396 | ||
5945640e LP |
1397 | return creds_main(argc, argv); |
1398 | } | |
1399 | ||
1400 | DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); |