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