]>
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" |
5945640e LP |
7 | #include "creds-util.h" |
8 | #include "dirent-util.h" | |
9 | #include "escape.h" | |
10 | #include "fileio.h" | |
11 | #include "format-table.h" | |
12 | #include "hexdecoct.h" | |
13 | #include "io-util.h" | |
14 | #include "json.h" | |
15 | #include "main-func.h" | |
16 | #include "memory-util.h" | |
17 | #include "missing_magic.h" | |
18 | #include "pager.h" | |
19 | #include "parse-argument.h" | |
20 | #include "pretty-print.h" | |
21 | #include "process-util.h" | |
22 | #include "stat-util.h" | |
23 | #include "string-table.h" | |
24 | #include "terminal-util.h" | |
75ddec93 | 25 | #include "tpm-pcr.h" |
5945640e LP |
26 | #include "tpm2-util.h" |
27 | #include "verbs.h" | |
28 | ||
29 | typedef enum TranscodeMode { | |
30 | TRANSCODE_OFF, | |
31 | TRANSCODE_BASE64, | |
32 | TRANSCODE_UNBASE64, | |
33 | TRANSCODE_HEX, | |
34 | TRANSCODE_UNHEX, | |
35 | _TRANSCODE_MAX, | |
36 | _TRANSCODE_INVALID = -EINVAL, | |
37 | } TranscodeMode; | |
38 | ||
39 | static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; | |
40 | static PagerFlags arg_pager_flags = 0; | |
41 | static bool arg_legend = true; | |
42 | static bool arg_system = false; | |
43 | static TranscodeMode arg_transcode = TRANSCODE_OFF; | |
44 | static int arg_newline = -1; | |
571d829e | 45 | static sd_id128_t arg_with_key = _CRED_AUTO; |
5945640e LP |
46 | static const char *arg_tpm2_device = NULL; |
47 | static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; | |
75ddec93 LP |
48 | static char *arg_tpm2_public_key = NULL; |
49 | static uint32_t arg_tpm2_public_key_pcr_mask = UINT32_MAX; | |
50 | static char *arg_tpm2_signature = NULL; | |
5945640e LP |
51 | static const char *arg_name = NULL; |
52 | static bool arg_name_any = false; | |
53 | static usec_t arg_timestamp = USEC_INFINITY; | |
54 | static usec_t arg_not_after = USEC_INFINITY; | |
55 | static bool arg_pretty = false; | |
6e0cb815 | 56 | static bool arg_quiet = false; |
5945640e | 57 | |
75ddec93 LP |
58 | STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); |
59 | STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); | |
60 | ||
5945640e LP |
61 | static const char* transcode_mode_table[_TRANSCODE_MAX] = { |
62 | [TRANSCODE_OFF] = "off", | |
63 | [TRANSCODE_BASE64] = "base64", | |
64 | [TRANSCODE_UNBASE64] = "unbase64", | |
65 | [TRANSCODE_HEX] = "hex", | |
66 | [TRANSCODE_UNHEX] = "unhex", | |
67 | }; | |
68 | ||
69 | DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(transcode_mode, TranscodeMode); | |
70 | ||
05eb896f | 71 | static int open_credential_directory( |
07a442ef | 72 | bool encrypted, |
05eb896f | 73 | DIR **ret_dir, |
07a442ef | 74 | const char **ret_prefix) { |
05eb896f | 75 | |
5945640e LP |
76 | const char *p; |
77 | DIR *d; | |
78 | int r; | |
79 | ||
05eb896f LP |
80 | assert(ret_dir); |
81 | ||
82 | if (arg_system) | |
83 | /* PID 1 ensures that system credentials are always accessible under the same fixed path. It | |
84 | * will create symlinks if necessary to guarantee that. */ | |
85 | p = encrypted ? | |
86 | ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY : | |
87 | SYSTEM_CREDENTIALS_DIRECTORY; | |
88 | else { | |
89 | /* Otherwise take the dirs from the env vars we got passed */ | |
90 | r = (encrypted ? get_encrypted_credentials_dir : get_credentials_dir)(&p); | |
91 | if (r == -ENXIO) /* No environment variable? */ | |
92 | goto not_found; | |
5945640e | 93 | if (r < 0) |
05eb896f LP |
94 | return log_error_errno(r, "Failed to get credentials directory: %m"); |
95 | } | |
5945640e | 96 | |
05eb896f LP |
97 | d = opendir(p); |
98 | if (!d) { | |
99 | /* No such dir? Then no creds where passed. (We conditionalize this on arg_system, since for | |
100 | * the per-service case a non-existing path would indicate an issue since the env var would | |
101 | * be set incorrectly in that case.) */ | |
102 | if (arg_system && errno == ENOENT) | |
103 | goto not_found; | |
104 | ||
105 | return log_error_errno(errno, "Failed to open credentials directory '%s': %m", p); | |
106 | } | |
5945640e | 107 | |
05eb896f | 108 | *ret_dir = d; |
5945640e | 109 | |
05eb896f LP |
110 | if (ret_prefix) |
111 | *ret_prefix = p; | |
5945640e | 112 | |
05eb896f LP |
113 | return 1; |
114 | ||
115 | not_found: | |
116 | *ret_dir = NULL; | |
117 | ||
118 | if (ret_prefix) | |
119 | *ret_prefix = NULL; | |
5945640e | 120 | |
5945640e LP |
121 | return 0; |
122 | } | |
123 | ||
05eb896f | 124 | static int add_credentials_to_table(Table *t, bool encrypted) { |
5945640e | 125 | _cleanup_(closedirp) DIR *d = NULL; |
05eb896f | 126 | const char *prefix; |
5945640e LP |
127 | int r; |
128 | ||
05eb896f | 129 | assert(t); |
5945640e | 130 | |
07a442ef | 131 | r = open_credential_directory(encrypted, &d, &prefix); |
05eb896f LP |
132 | if (r < 0) |
133 | return r; | |
134 | if (!d) | |
135 | return 0; /* No creds dir set */ | |
5945640e LP |
136 | |
137 | for (;;) { | |
05eb896f | 138 | _cleanup_free_ char *j = NULL; |
5945640e | 139 | const char *secure, *secure_color = NULL; |
254d1313 | 140 | _cleanup_close_ int fd = -EBADF; |
5945640e LP |
141 | struct dirent *de; |
142 | struct stat st; | |
143 | ||
144 | errno = 0; | |
145 | de = readdir_no_dot(d); | |
146 | if (!de) { | |
147 | if (errno == 0) | |
148 | break; | |
149 | ||
150 | return log_error_errno(errno, "Failed to read credentials directory: %m"); | |
151 | } | |
152 | ||
153 | if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN)) | |
154 | continue; | |
155 | ||
156 | if (!credential_name_valid(de->d_name)) | |
157 | continue; | |
158 | ||
159 | fd = openat(dirfd(d), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW); | |
160 | if (fd < 0) { | |
161 | if (errno == ENOENT) /* Vanished by now? */ | |
162 | continue; | |
163 | ||
164 | return log_error_errno(errno, "Failed to open credential '%s': %m", de->d_name); | |
165 | } | |
166 | ||
167 | if (fstat(fd, &st) < 0) | |
168 | return log_error_errno(errno, "Failed to stat credential '%s': %m", de->d_name); | |
169 | ||
170 | if (!S_ISREG(st.st_mode)) | |
171 | continue; | |
172 | ||
05eb896f LP |
173 | if (encrypted) { |
174 | secure = "encrypted"; | |
175 | secure_color = ansi_highlight_green(); | |
176 | } else if ((st.st_mode & 0377) != 0) { | |
5945640e LP |
177 | secure = "insecure"; /* Anything that is accessible more than read-only to its owner is insecure */ |
178 | secure_color = ansi_highlight_red(); | |
179 | } else { | |
180 | r = fd_is_fs_type(fd, RAMFS_MAGIC); | |
181 | if (r < 0) | |
182 | return log_error_errno(r, "Failed to determine backing file system of '%s': %m", de->d_name); | |
183 | ||
184 | secure = r ? "secure" : "weak"; /* ramfs is not swappable, hence "secure", everything else is "weak" */ | |
185 | secure_color = r ? ansi_highlight_green() : ansi_highlight_yellow4(); | |
186 | } | |
187 | ||
05eb896f LP |
188 | j = path_join(prefix, de->d_name); |
189 | if (!j) | |
190 | return log_oom(); | |
191 | ||
5945640e LP |
192 | r = table_add_many( |
193 | t, | |
194 | TABLE_STRING, de->d_name, | |
195 | TABLE_STRING, secure, | |
196 | TABLE_SET_COLOR, secure_color, | |
05eb896f LP |
197 | TABLE_SIZE, (uint64_t) st.st_size, |
198 | TABLE_STRING, j); | |
5945640e LP |
199 | if (r < 0) |
200 | return table_log_add_error(r); | |
201 | } | |
202 | ||
05eb896f LP |
203 | return 1; /* Creds dir set */ |
204 | } | |
205 | ||
206 | static int verb_list(int argc, char **argv, void *userdata) { | |
207 | _cleanup_(table_unrefp) Table *t = NULL; | |
208 | int r, q; | |
209 | ||
210 | t = table_new("name", "secure", "size", "path"); | |
211 | if (!t) | |
212 | return log_oom(); | |
213 | ||
214 | (void) table_set_align_percent(t, table_get_cell(t, 0, 2), 100); | |
215 | ||
216 | r = add_credentials_to_table(t, /* encrypted= */ true); | |
217 | if (r < 0) | |
218 | return r; | |
219 | ||
220 | q = add_credentials_to_table(t, /* encrypted= */ false); | |
221 | if (q < 0) | |
222 | return q; | |
223 | ||
224 | if (r == 0 && q == 0) { | |
225 | if (arg_system) | |
226 | return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed to system."); | |
227 | ||
228 | return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed. (i.e. $CREDENTIALS_DIRECTORY not set.)"); | |
229 | } | |
230 | ||
5945640e LP |
231 | if ((arg_json_format_flags & JSON_FORMAT_OFF) && table_get_rows(t) <= 1) { |
232 | log_info("No credentials"); | |
233 | return 0; | |
234 | } | |
235 | ||
236 | return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); | |
237 | } | |
238 | ||
239 | static int transcode( | |
240 | const void *input, | |
241 | size_t input_size, | |
242 | void **ret_output, | |
243 | size_t *ret_output_size) { | |
244 | ||
245 | int r; | |
246 | ||
247 | assert(input); | |
248 | assert(input_size); | |
249 | assert(ret_output); | |
250 | assert(ret_output_size); | |
251 | ||
252 | switch (arg_transcode) { | |
253 | ||
254 | case TRANSCODE_BASE64: { | |
bc1f27ff | 255 | char *buf; |
5945640e LP |
256 | ssize_t l; |
257 | ||
258 | l = base64mem_full(input, input_size, 79, &buf); | |
259 | if (l < 0) | |
260 | return l; | |
261 | ||
262 | *ret_output = buf; | |
263 | *ret_output_size = l; | |
264 | return 0; | |
265 | } | |
266 | ||
267 | case TRANSCODE_UNBASE64: | |
268 | r = unbase64mem_full(input, input_size, true, ret_output, ret_output_size); | |
269 | if (r == -EPIPE) /* Uneven number of chars */ | |
270 | return -EINVAL; | |
271 | ||
272 | return r; | |
273 | ||
274 | case TRANSCODE_HEX: { | |
275 | char *buf; | |
276 | ||
277 | buf = hexmem(input, input_size); | |
278 | if (!buf) | |
279 | return -ENOMEM; | |
280 | ||
281 | *ret_output = buf; | |
282 | *ret_output_size = input_size * 2; | |
283 | return 0; | |
284 | } | |
285 | ||
286 | case TRANSCODE_UNHEX: | |
287 | r = unhexmem_full(input, input_size, true, ret_output, ret_output_size); | |
288 | if (r == -EPIPE) /* Uneven number of chars */ | |
289 | return -EINVAL; | |
290 | ||
291 | return r; | |
292 | ||
293 | default: | |
04499a70 | 294 | assert_not_reached(); |
5945640e LP |
295 | } |
296 | } | |
297 | ||
298 | static int print_newline(FILE *f, const char *data, size_t l) { | |
299 | int fd; | |
300 | ||
301 | assert(f); | |
302 | assert(data || l == 0); | |
303 | ||
304 | /* If turned off explicitly, don't print newline */ | |
305 | if (arg_newline == 0) | |
306 | return 0; | |
307 | ||
308 | /* If data already has newline, don't print either */ | |
309 | if (l > 0 && data[l-1] == '\n') | |
310 | return 0; | |
311 | ||
312 | /* Don't bother unless this is a tty */ | |
313 | fd = fileno(f); | |
314 | if (fd >= 0 && isatty(fd) <= 0) | |
315 | return 0; | |
316 | ||
317 | if (fputc('\n', f) != '\n') | |
318 | return log_error_errno(errno, "Failed to write trailing newline: %m"); | |
319 | ||
320 | return 1; | |
321 | } | |
322 | ||
323 | static int write_blob(FILE *f, const void *data, size_t size) { | |
324 | _cleanup_(erase_and_freep) void *transcoded = NULL; | |
325 | int r; | |
326 | ||
327 | if (arg_transcode == TRANSCODE_OFF && | |
328 | arg_json_format_flags != JSON_FORMAT_OFF) { | |
329 | ||
330 | _cleanup_(erase_and_freep) char *suffixed = NULL; | |
331 | _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; | |
332 | ||
333 | if (memchr(data, 0, size)) | |
334 | return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Credential data contains embedded NUL, can't parse as JSON."); | |
335 | ||
336 | suffixed = memdup_suffix0(data, size); | |
337 | if (!suffixed) | |
338 | return log_oom(); | |
339 | ||
340 | r = json_parse(suffixed, JSON_PARSE_SENSITIVE, &v, NULL, NULL); | |
341 | if (r < 0) | |
342 | return log_error_errno(r, "Failed to parse JSON: %m"); | |
343 | ||
344 | json_variant_dump(v, arg_json_format_flags, f, NULL); | |
345 | return 0; | |
346 | } | |
347 | ||
348 | if (arg_transcode != TRANSCODE_OFF) { | |
349 | r = transcode(data, size, &transcoded, &size); | |
350 | if (r < 0) | |
351 | return log_error_errno(r, "Failed to transcode data: %m"); | |
352 | ||
353 | data = transcoded; | |
354 | } | |
355 | ||
356 | if (fwrite(data, 1, size, f) != size) | |
357 | return log_error_errno(errno, "Failed to write credential data: %m"); | |
358 | ||
359 | r = print_newline(f, data, size); | |
360 | if (r < 0) | |
361 | return r; | |
362 | ||
363 | if (fflush(f) != 0) | |
364 | return log_error_errno(errno, "Failed to flush output: %m"); | |
365 | ||
366 | return 0; | |
367 | } | |
368 | ||
369 | static int verb_cat(int argc, char **argv, void *userdata) { | |
05eb896f | 370 | usec_t timestamp; |
5945640e | 371 | int r, ret = 0; |
5945640e | 372 | |
05eb896f | 373 | timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); |
5945640e LP |
374 | |
375 | STRV_FOREACH(cn, strv_skip(argv, 1)) { | |
376 | _cleanup_(erase_and_freep) void *data = NULL; | |
377 | size_t size = 0; | |
05eb896f | 378 | int encrypted; |
5945640e LP |
379 | |
380 | if (!credential_name_valid(*cn)) { | |
381 | log_error("Credential name '%s' is not valid.", *cn); | |
382 | if (ret >= 0) | |
383 | ret = -EINVAL; | |
384 | continue; | |
385 | } | |
386 | ||
05eb896f | 387 | /* Look both in regular and in encrypted credentials */ |
07a442ef | 388 | for (encrypted = 0; encrypted < 2; encrypted++) { |
05eb896f LP |
389 | _cleanup_(closedirp) DIR *d = NULL; |
390 | ||
07a442ef | 391 | r = open_credential_directory(encrypted, &d, NULL); |
05eb896f LP |
392 | if (r < 0) |
393 | return log_error_errno(r, "Failed to open credentials directory: %m"); | |
394 | if (!d) /* Not set */ | |
395 | continue; | |
396 | ||
397 | r = read_full_file_full( | |
398 | dirfd(d), *cn, | |
399 | UINT64_MAX, SIZE_MAX, | |
400 | READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE, | |
401 | NULL, | |
402 | (char**) &data, &size); | |
403 | if (r == -ENOENT) /* Not found */ | |
404 | continue; | |
405 | if (r >= 0) /* Found */ | |
406 | break; | |
407 | ||
5945640e LP |
408 | log_error_errno(r, "Failed to read credential '%s': %m", *cn); |
409 | if (ret >= 0) | |
410 | ret = r; | |
05eb896f LP |
411 | } |
412 | ||
413 | if (encrypted >= 2) { /* Found nowhere */ | |
414 | log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Credential '%s' not set.", *cn); | |
415 | if (ret >= 0) | |
416 | ret = -ENOENT; | |
417 | ||
5945640e LP |
418 | continue; |
419 | } | |
420 | ||
05eb896f LP |
421 | if (encrypted) { |
422 | _cleanup_(erase_and_freep) void *plaintext = NULL; | |
423 | size_t plaintext_size; | |
424 | ||
425 | r = decrypt_credential_and_warn( | |
426 | *cn, | |
427 | timestamp, | |
428 | arg_tpm2_device, | |
75ddec93 | 429 | arg_tpm2_signature, |
05eb896f LP |
430 | data, size, |
431 | &plaintext, &plaintext_size); | |
432 | if (r < 0) | |
433 | return r; | |
434 | ||
435 | erase_and_free(data); | |
436 | data = TAKE_PTR(plaintext); | |
437 | size = plaintext_size; | |
438 | } | |
439 | ||
5945640e LP |
440 | r = write_blob(stdout, data, size); |
441 | if (r < 0) | |
442 | return r; | |
443 | } | |
444 | ||
445 | return ret; | |
446 | } | |
447 | ||
448 | static int verb_encrypt(int argc, char **argv, void *userdata) { | |
449 | _cleanup_free_ char *base64_buf = NULL, *fname = NULL; | |
450 | _cleanup_(erase_and_freep) char *plaintext = NULL; | |
451 | const char *input_path, *output_path, *name; | |
452 | _cleanup_free_ void *output = NULL; | |
453 | size_t plaintext_size, output_size; | |
454 | ssize_t base64_size; | |
455 | usec_t timestamp; | |
456 | int r; | |
457 | ||
458 | assert(argc == 3); | |
459 | ||
2f092762 | 460 | input_path = empty_or_dash(argv[1]) ? NULL : argv[1]; |
5945640e LP |
461 | |
462 | if (input_path) | |
463 | r = read_full_file_full(AT_FDCWD, input_path, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &plaintext, &plaintext_size); | |
464 | else | |
465 | r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, &plaintext, &plaintext_size); | |
466 | if (r == -E2BIG) | |
467 | return log_error_errno(r, "Plaintext too long for credential (allowed size: %zu).", (size_t) CREDENTIAL_SIZE_MAX); | |
468 | if (r < 0) | |
469 | return log_error_errno(r, "Failed to read plaintext: %m"); | |
470 | ||
2f092762 | 471 | output_path = empty_or_dash(argv[2]) ? NULL : argv[2]; |
5945640e LP |
472 | |
473 | if (arg_name_any) | |
474 | name = NULL; | |
475 | else if (arg_name) | |
476 | name = arg_name; | |
477 | else if (output_path) { | |
478 | r = path_extract_filename(output_path, &fname); | |
479 | if (r < 0) | |
480 | return log_error_errno(r, "Failed to extract filename from '%s': %m", output_path); | |
481 | if (r == O_DIRECTORY) | |
482 | return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", output_path); | |
483 | ||
484 | name = fname; | |
485 | } else { | |
486 | log_warning("No credential name specified, not embedding credential name in encrypted data. (Disable this warning with --name=)"); | |
487 | name = NULL; | |
488 | } | |
489 | ||
490 | timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); | |
491 | ||
492 | if (arg_not_after != USEC_INFINITY && arg_not_after < timestamp) | |
493 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid."); | |
494 | ||
495 | r = encrypt_credential_and_warn( | |
496 | arg_with_key, | |
497 | name, | |
498 | timestamp, | |
499 | arg_not_after, | |
500 | arg_tpm2_device, | |
501 | arg_tpm2_pcr_mask, | |
75ddec93 LP |
502 | arg_tpm2_public_key, |
503 | arg_tpm2_public_key_pcr_mask, | |
5945640e LP |
504 | plaintext, plaintext_size, |
505 | &output, &output_size); | |
506 | if (r < 0) | |
507 | return r; | |
508 | ||
509 | base64_size = base64mem_full(output, output_size, arg_pretty ? 69 : 79, &base64_buf); | |
510 | if (base64_size < 0) | |
511 | return base64_size; | |
512 | ||
513 | if (arg_pretty) { | |
514 | _cleanup_free_ char *escaped = NULL, *indented = NULL, *j = NULL; | |
515 | ||
516 | if (name) { | |
517 | escaped = cescape(name); | |
518 | if (!escaped) | |
519 | return log_oom(); | |
520 | } | |
521 | ||
522 | indented = strreplace(base64_buf, "\n", " \\\n "); | |
523 | if (!indented) | |
524 | return log_oom(); | |
525 | ||
526 | j = strjoin("SetCredentialEncrypted=", name, ": \\\n ", indented, "\n"); | |
527 | if (!j) | |
528 | return log_oom(); | |
529 | ||
530 | free_and_replace(base64_buf, j); | |
531 | } | |
532 | ||
533 | if (output_path) | |
534 | r = write_string_file(output_path, base64_buf, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE); | |
535 | else | |
536 | r = write_string_stream(stdout, base64_buf, 0); | |
537 | if (r < 0) | |
538 | return log_error_errno(r, "Failed to write result: %m"); | |
539 | ||
540 | return EXIT_SUCCESS; | |
541 | } | |
542 | ||
543 | static int verb_decrypt(int argc, char **argv, void *userdata) { | |
544 | _cleanup_(erase_and_freep) void *plaintext = NULL; | |
545 | _cleanup_free_ char *input = NULL, *fname = NULL; | |
546 | _cleanup_fclose_ FILE *output_file = NULL; | |
547 | const char *input_path, *output_path, *name; | |
548 | size_t input_size, plaintext_size; | |
549 | usec_t timestamp; | |
550 | FILE *f; | |
551 | int r; | |
552 | ||
553 | assert(IN_SET(argc, 2, 3)); | |
554 | ||
2f092762 | 555 | input_path = empty_or_dash(argv[1]) ? NULL : argv[1]; |
5945640e LP |
556 | |
557 | if (input_path) | |
558 | r = read_full_file_full(AT_FDCWD, argv[1], UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &input, &input_size); | |
559 | else | |
560 | r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, &input, &input_size); | |
561 | if (r == -E2BIG) | |
562 | return log_error_errno(r, "Data too long for encrypted credential (allowed size: %zu).", (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX); | |
563 | if (r < 0) | |
564 | return log_error_errno(r, "Failed to read encrypted credential data: %m"); | |
565 | ||
566 | output_path = (argc < 3 || isempty(argv[2]) || streq(argv[2], "-")) ? NULL : argv[2]; | |
567 | ||
568 | if (arg_name_any) | |
569 | name = NULL; | |
570 | else if (arg_name) | |
571 | name = arg_name; | |
572 | else if (input_path) { | |
573 | r = path_extract_filename(input_path, &fname); | |
574 | if (r < 0) | |
575 | return log_error_errno(r, "Failed to extract filename from '%s': %m", input_path); | |
576 | if (r == O_DIRECTORY) | |
577 | return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Path '%s' refers to directory, refusing.", input_path); | |
578 | ||
579 | name = fname; | |
580 | } else { | |
581 | log_warning("No credential name specified, not validating credential name embedded in encrypted data. (Disable this warning with --name=.)"); | |
582 | name = NULL; | |
583 | } | |
584 | ||
585 | timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); | |
586 | ||
587 | r = decrypt_credential_and_warn( | |
588 | name, | |
589 | timestamp, | |
590 | arg_tpm2_device, | |
75ddec93 | 591 | arg_tpm2_signature, |
5945640e LP |
592 | input, input_size, |
593 | &plaintext, &plaintext_size); | |
594 | if (r < 0) | |
595 | return r; | |
596 | ||
597 | if (output_path) { | |
598 | output_file = fopen(output_path, "we"); | |
599 | if (!output_file) | |
600 | return log_error_errno(errno, "Failed to create output file '%s': %m", output_path); | |
601 | ||
602 | f = output_file; | |
603 | } else | |
604 | f = stdout; | |
605 | ||
606 | r = write_blob(f, plaintext, plaintext_size); | |
607 | if (r < 0) | |
608 | return r; | |
609 | ||
610 | return EXIT_SUCCESS; | |
611 | } | |
612 | ||
613 | static int verb_setup(int argc, char **argv, void *userdata) { | |
614 | size_t size; | |
615 | int r; | |
616 | ||
617 | r = get_credential_host_secret(CREDENTIAL_SECRET_GENERATE|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED, NULL, &size); | |
618 | if (r < 0) | |
619 | return log_error_errno(r, "Failed to setup credentials host key: %m"); | |
620 | ||
621 | log_info("%zu byte credentials host key set up.", size); | |
622 | ||
623 | return EXIT_SUCCESS; | |
624 | } | |
625 | ||
6e0cb815 LP |
626 | static int verb_has_tpm2(int argc, char **argv, void *userdata) { |
627 | Tpm2Support s; | |
628 | ||
629 | s = tpm2_support(); | |
630 | ||
631 | if (!arg_quiet) { | |
632 | if (s == TPM2_SUPPORT_FULL) | |
633 | puts("yes"); | |
634 | else if (s == TPM2_SUPPORT_NONE) | |
635 | puts("no"); | |
636 | else | |
637 | puts("partial"); | |
638 | ||
639 | printf("%sfirmware\n" | |
640 | "%sdriver\n" | |
300bba79 DDM |
641 | "%ssystem\n" |
642 | "%ssubsystem\n", | |
6e0cb815 LP |
643 | plus_minus(s & TPM2_SUPPORT_FIRMWARE), |
644 | plus_minus(s & TPM2_SUPPORT_DRIVER), | |
300bba79 DDM |
645 | plus_minus(s & TPM2_SUPPORT_SYSTEM), |
646 | plus_minus(s & TPM2_SUPPORT_SUBSYSTEM)); | |
6e0cb815 LP |
647 | } |
648 | ||
649 | /* Return inverted bit flags. So that TPM2_SUPPORT_FULL becomes EXIT_SUCCESS and the other values | |
650 | * become some reasonable values 1…7. i.e. the flags we return here tell what is missing rather than | |
8ac6b05b | 651 | * what is there, acknowledging the fact that for process exit statuses it is customary to return |
6e0cb815 LP |
652 | * zero (EXIT_FAILURE) when all is good, instead of all being bad. */ |
653 | return ~s & TPM2_SUPPORT_FULL; | |
654 | } | |
655 | ||
5945640e LP |
656 | static int verb_help(int argc, char **argv, void *userdata) { |
657 | _cleanup_free_ char *link = NULL; | |
658 | int r; | |
659 | ||
660 | r = terminal_urlify_man("systemd-creds", "1", &link); | |
661 | if (r < 0) | |
662 | return log_oom(); | |
663 | ||
664 | printf("%1$s [OPTIONS...] COMMAND ...\n" | |
665 | "\n%5$sDisplay and Process Credentials.%6$s\n" | |
666 | "\n%3$sCommands:%4$s\n" | |
667 | " list Show installed and available versions\n" | |
668 | " cat CREDENTIAL... Show specified credentials\n" | |
669 | " setup Generate credentials host key, if not existing yet\n" | |
670 | " encrypt INPUT OUTPUT Encrypt plaintext credential file and write to\n" | |
671 | " ciphertext credential file\n" | |
672 | " decrypt INPUT [OUTPUT] Decrypt ciphertext credential file and write to\n" | |
673 | " plaintext credential file\n" | |
6e0cb815 | 674 | " has-tpm2 Report whether TPM2 support is available\n" |
5945640e LP |
675 | " -h --help Show this help\n" |
676 | " --version Show package version\n" | |
677 | "\n%3$sOptions:%4$s\n" | |
678 | " --no-pager Do not pipe output into a pager\n" | |
679 | " --no-legend Do not show the headers and footers\n" | |
680 | " --json=pretty|short|off\n" | |
681 | " Generate JSON output\n" | |
682 | " --system Show credentials passed to system\n" | |
683 | " --transcode=base64|unbase64|hex|unhex\n" | |
684 | " Transcode credential data\n" | |
685 | " --newline=auto|yes|no\n" | |
686 | " Suffix output with newline\n" | |
687 | " -p --pretty Output as SetCredentialEncrypted= line\n" | |
688 | " --name=NAME Override filename included in encrypted credential\n" | |
689 | " --timestamp=TIME Include specified timestamp in encrypted credential\n" | |
690 | " --not-after=TIME Include specified invalidation time in encrypted\n" | |
691 | " credential\n" | |
b6553329 | 692 | " --with-key=host|tpm2|host+tpm2|tpm2-absent|auto|auto-initrd\n" |
5945640e LP |
693 | " Which keys to encrypt with\n" |
694 | " -H Shortcut for --with-key=host\n" | |
695 | " -T Shortcut for --with-key=tpm2\n" | |
696 | " --tpm2-device=PATH\n" | |
697 | " Pick TPM2 device\n" | |
698 | " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n" | |
75ddec93 LP |
699 | " Specify TPM2 PCRs to seal against (fixed hash)\n" |
700 | " --tpm2-public-key=PATH\n" | |
701 | " Specify PEM certificate to seal against\n" | |
702 | " --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n" | |
703 | " Specify TPM2 PCRs to seal against (public key)\n" | |
704 | " --tpm2-signature=PATH\n" | |
705 | " Specify signature for public key PCR policy\n" | |
6e0cb815 | 706 | " -q --quiet Suppress output for 'has-tpm2' verb\n" |
5945640e LP |
707 | "\nSee the %2$s for details.\n" |
708 | , program_invocation_short_name | |
709 | , link | |
710 | , ansi_underline(), ansi_normal() | |
711 | , ansi_highlight(), ansi_normal() | |
712 | ); | |
713 | ||
714 | return 0; | |
715 | } | |
716 | ||
717 | static int parse_argv(int argc, char *argv[]) { | |
718 | ||
719 | enum { | |
720 | ARG_VERSION = 0x100, | |
721 | ARG_NO_PAGER, | |
722 | ARG_NO_LEGEND, | |
723 | ARG_JSON, | |
724 | ARG_SYSTEM, | |
725 | ARG_TRANSCODE, | |
726 | ARG_NEWLINE, | |
727 | ARG_WITH_KEY, | |
728 | ARG_TPM2_DEVICE, | |
729 | ARG_TPM2_PCRS, | |
75ddec93 LP |
730 | ARG_TPM2_PUBLIC_KEY, |
731 | ARG_TPM2_PUBLIC_KEY_PCRS, | |
732 | ARG_TPM2_SIGNATURE, | |
5945640e LP |
733 | ARG_NAME, |
734 | ARG_TIMESTAMP, | |
735 | ARG_NOT_AFTER, | |
736 | }; | |
737 | ||
738 | static const struct option options[] = { | |
75ddec93 LP |
739 | { "help", no_argument, NULL, 'h' }, |
740 | { "version", no_argument, NULL, ARG_VERSION }, | |
741 | { "no-pager", no_argument, NULL, ARG_NO_PAGER }, | |
742 | { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, | |
743 | { "json", required_argument, NULL, ARG_JSON }, | |
744 | { "system", no_argument, NULL, ARG_SYSTEM }, | |
745 | { "transcode", required_argument, NULL, ARG_TRANSCODE }, | |
746 | { "newline", required_argument, NULL, ARG_NEWLINE }, | |
747 | { "pretty", no_argument, NULL, 'p' }, | |
748 | { "with-key", required_argument, NULL, ARG_WITH_KEY }, | |
749 | { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, | |
750 | { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, | |
751 | { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY }, | |
752 | { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS }, | |
753 | { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE }, | |
754 | { "name", required_argument, NULL, ARG_NAME }, | |
755 | { "timestamp", required_argument, NULL, ARG_TIMESTAMP }, | |
756 | { "not-after", required_argument, NULL, ARG_NOT_AFTER }, | |
757 | { "quiet", no_argument, NULL, 'q' }, | |
5945640e LP |
758 | {} |
759 | }; | |
760 | ||
761 | int c, r; | |
762 | ||
763 | assert(argc >= 0); | |
764 | assert(argv); | |
765 | ||
6e0cb815 | 766 | while ((c = getopt_long(argc, argv, "hHTpq", options, NULL)) >= 0) { |
5945640e LP |
767 | |
768 | switch (c) { | |
769 | ||
770 | case 'h': | |
771 | return verb_help(0, NULL, NULL); | |
772 | ||
773 | case ARG_VERSION: | |
774 | return version(); | |
775 | ||
776 | case ARG_NO_PAGER: | |
777 | arg_pager_flags |= PAGER_DISABLE; | |
778 | break; | |
779 | ||
780 | case ARG_NO_LEGEND: | |
781 | arg_legend = false; | |
782 | break; | |
783 | ||
784 | case ARG_JSON: | |
785 | r = parse_json_argument(optarg, &arg_json_format_flags); | |
786 | if (r <= 0) | |
787 | return r; | |
788 | ||
789 | break; | |
790 | ||
791 | case ARG_SYSTEM: | |
792 | arg_system = true; | |
793 | break; | |
794 | ||
795 | case ARG_TRANSCODE: | |
796 | if (parse_boolean(optarg) == 0) /* If specified as "false", turn transcoding off */ | |
797 | arg_transcode = TRANSCODE_OFF; | |
798 | else { | |
799 | TranscodeMode m; | |
800 | ||
801 | m = transcode_mode_from_string(optarg); | |
802 | if (m < 0) | |
803 | return log_error_errno(m, "Failed to parse transcode mode: %m"); | |
804 | ||
805 | arg_transcode = m; | |
806 | } | |
807 | ||
808 | break; | |
809 | ||
810 | case ARG_NEWLINE: | |
811 | if (isempty(optarg) || streq(optarg, "auto")) | |
812 | arg_newline = -1; | |
813 | else { | |
814 | bool b; | |
815 | ||
816 | r = parse_boolean_argument("--newline=", optarg, &b); | |
817 | if (r < 0) | |
818 | return r; | |
819 | ||
820 | arg_newline = b; | |
821 | } | |
822 | break; | |
823 | ||
824 | case 'p': | |
825 | arg_pretty = true; | |
826 | break; | |
827 | ||
828 | case ARG_WITH_KEY: | |
829 | if (isempty(optarg) || streq(optarg, "auto")) | |
571d829e | 830 | arg_with_key = _CRED_AUTO; |
b6553329 LP |
831 | else if (streq(optarg, "auto-initrd")) |
832 | arg_with_key = _CRED_AUTO_INITRD; | |
5945640e LP |
833 | else if (streq(optarg, "host")) |
834 | arg_with_key = CRED_AES256_GCM_BY_HOST; | |
835 | else if (streq(optarg, "tpm2")) | |
836 | arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC; | |
75ddec93 LP |
837 | else if (streq(optarg, "tpm2-with-public-key")) |
838 | arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK; | |
5945640e LP |
839 | else if (STR_IN_SET(optarg, "host+tpm2", "tpm2+host")) |
840 | arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC; | |
75ddec93 LP |
841 | else if (STR_IN_SET(optarg, "host+tpm2-with-public-key", "tpm2-with-public-key+host")) |
842 | arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK; | |
b6553329 LP |
843 | else if (streq(optarg, "tpm2-absent")) |
844 | arg_with_key = CRED_AES256_GCM_BY_TPM2_ABSENT; | |
5945640e LP |
845 | else |
846 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown key type: %s", optarg); | |
847 | ||
848 | break; | |
849 | ||
850 | case 'H': | |
851 | arg_with_key = CRED_AES256_GCM_BY_HOST; | |
852 | break; | |
853 | ||
854 | case 'T': | |
855 | arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC; | |
856 | break; | |
857 | ||
d1829af9 | 858 | case ARG_TPM2_DEVICE: |
5945640e LP |
859 | if (streq(optarg, "list")) |
860 | return tpm2_list_devices(); | |
861 | ||
d1829af9 | 862 | arg_tpm2_device = streq(optarg, "auto") ? NULL : optarg; |
5945640e | 863 | break; |
5945640e | 864 | |
75ddec93 | 865 | case ARG_TPM2_PCRS: /* For fixed hash PCR policies only */ |
222a951f | 866 | r = tpm2_parse_pcr_argument(optarg, &arg_tpm2_pcr_mask); |
5945640e LP |
867 | if (r < 0) |
868 | return r; | |
869 | ||
5945640e | 870 | break; |
5945640e | 871 | |
75ddec93 LP |
872 | case ARG_TPM2_PUBLIC_KEY: |
873 | r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key); | |
874 | if (r < 0) | |
875 | return r; | |
876 | ||
877 | break; | |
878 | ||
879 | case ARG_TPM2_PUBLIC_KEY_PCRS: /* For public key PCR policies only */ | |
880 | r = tpm2_parse_pcr_argument(optarg, &arg_tpm2_public_key_pcr_mask); | |
881 | if (r < 0) | |
882 | return r; | |
883 | ||
884 | break; | |
885 | ||
886 | case ARG_TPM2_SIGNATURE: | |
887 | r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature); | |
888 | if (r < 0) | |
889 | return r; | |
890 | ||
891 | break; | |
892 | ||
5945640e LP |
893 | case ARG_NAME: |
894 | if (isempty(optarg)) { | |
895 | arg_name = NULL; | |
896 | arg_name_any = true; | |
897 | break; | |
898 | } | |
899 | ||
900 | if (!credential_name_valid(optarg)) | |
901 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", optarg); | |
902 | ||
903 | arg_name = optarg; | |
904 | arg_name_any = false; | |
905 | break; | |
906 | ||
907 | case ARG_TIMESTAMP: | |
908 | r = parse_timestamp(optarg, &arg_timestamp); | |
909 | if (r < 0) | |
910 | return log_error_errno(r, "Failed to parse timestamp: %s", optarg); | |
911 | ||
912 | break; | |
913 | ||
914 | case ARG_NOT_AFTER: | |
915 | r = parse_timestamp(optarg, &arg_not_after); | |
916 | if (r < 0) | |
917 | return log_error_errno(r, "Failed to parse --not-after= timestamp: %s", optarg); | |
918 | ||
919 | break; | |
920 | ||
6e0cb815 LP |
921 | case 'q': |
922 | arg_quiet = true; | |
923 | break; | |
924 | ||
5945640e LP |
925 | case '?': |
926 | return -EINVAL; | |
927 | ||
928 | default: | |
04499a70 | 929 | assert_not_reached(); |
5945640e LP |
930 | } |
931 | } | |
932 | ||
933 | if (arg_tpm2_pcr_mask == UINT32_MAX) | |
934 | arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; | |
75ddec93 LP |
935 | if (arg_tpm2_public_key_pcr_mask == UINT32_MAX) |
936 | arg_tpm2_public_key_pcr_mask = UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE; | |
5945640e LP |
937 | |
938 | return 1; | |
939 | } | |
940 | ||
941 | static int creds_main(int argc, char *argv[]) { | |
942 | ||
943 | static const Verb verbs[] = { | |
6e0cb815 LP |
944 | { "list", VERB_ANY, 1, VERB_DEFAULT, verb_list }, |
945 | { "cat", 2, VERB_ANY, 0, verb_cat }, | |
946 | { "encrypt", 3, 3, 0, verb_encrypt }, | |
947 | { "decrypt", 2, 3, 0, verb_decrypt }, | |
948 | { "setup", VERB_ANY, 1, 0, verb_setup }, | |
949 | { "help", VERB_ANY, 1, 0, verb_help }, | |
950 | { "has-tpm2", VERB_ANY, 1, 0, verb_has_tpm2 }, | |
5945640e LP |
951 | {} |
952 | }; | |
953 | ||
954 | return dispatch_verb(argc, argv, verbs, NULL); | |
955 | } | |
956 | ||
957 | static int run(int argc, char *argv[]) { | |
958 | int r; | |
959 | ||
960 | log_setup(); | |
961 | ||
962 | r = parse_argv(argc, argv); | |
963 | if (r <= 0) | |
964 | return r; | |
965 | ||
966 | return creds_main(argc, argv); | |
967 | } | |
968 | ||
969 | DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); |