]>
Commit | Line | Data |
---|---|---|
708d7524 LP |
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | ||
3 | #include <getopt.h> | |
4 | ||
5 | #include <sd-messages.h> | |
6 | ||
d6b4d1c7 | 7 | #include "build.h" |
6c51b49c | 8 | #include "efi-loader.h" |
17984c55 | 9 | #include "escape.h" |
708d7524 LP |
10 | #include "main-func.h" |
11 | #include "openssl-util.h" | |
17984c55 | 12 | #include "parse-argument.h" |
b0d00ec6 | 13 | #include "parse-util.h" |
4cdef9f0 | 14 | #include "pcrextend-util.h" |
708d7524 | 15 | #include "pretty-print.h" |
4cdef9f0 | 16 | #include "strv.h" |
e0e1f4f7 | 17 | #include "tpm2-pcr.h" |
708d7524 | 18 | #include "tpm2-util.h" |
4e16d5c6 LP |
19 | #include "varlink.h" |
20 | #include "varlink-io.systemd.PCRExtend.h" | |
708d7524 | 21 | |
0318d545 | 22 | static bool arg_graceful = false; |
708d7524 LP |
23 | static char *arg_tpm2_device = NULL; |
24 | static char **arg_banks = NULL; | |
17984c55 LP |
25 | static char *arg_file_system = NULL; |
26 | static bool arg_machine_id = false; | |
b0d00ec6 | 27 | static unsigned arg_pcr_index = UINT_MAX; |
4e16d5c6 | 28 | static bool arg_varlink = false; |
708d7524 LP |
29 | |
30 | STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); | |
31 | STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); | |
17984c55 | 32 | STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep); |
708d7524 | 33 | |
4e16d5c6 LP |
34 | #define EXTENSION_STRING_SAFE_LIMIT 1024 |
35 | ||
708d7524 LP |
36 | static int help(int argc, char *argv[], void *userdata) { |
37 | _cleanup_free_ char *link = NULL; | |
38 | int r; | |
39 | ||
32295fa0 | 40 | r = terminal_urlify_man("systemd-pcrextend", "8", &link); |
708d7524 LP |
41 | if (r < 0) |
42 | return log_oom(); | |
43 | ||
17984c55 LP |
44 | printf("%1$s [OPTIONS...] WORD\n" |
45 | "%1$s [OPTIONS...] --file-system=PATH\n" | |
46 | "%1$s [OPTIONS...] --machine-id\n" | |
32295fa0 | 47 | "\n%5$sExtend a TPM2 PCR with boot phase, machine ID, or file system ID.%6$s\n" |
708d7524 LP |
48 | "\n%3$sOptions:%4$s\n" |
49 | " -h --help Show this help\n" | |
50 | " --version Print version\n" | |
b0d00ec6 LP |
51 | " --bank=DIGEST Select TPM PCR bank (SHA1, SHA256)\n" |
52 | " --pcr=INDEX Select TPM PCR index (0…23)\n" | |
708d7524 | 53 | " --tpm2-device=PATH Use specified TPM2 device\n" |
0318d545 | 54 | " --graceful Exit gracefully if no TPM2 device is found\n" |
17984c55 LP |
55 | " --file-system=PATH Measure UUID/labels of file system into PCR 15\n" |
56 | " --machine-id Measure machine ID into PCR 15\n" | |
708d7524 LP |
57 | "\nSee the %2$s for details.\n", |
58 | program_invocation_short_name, | |
59 | link, | |
60 | ansi_underline(), | |
61 | ansi_normal(), | |
62 | ansi_highlight(), | |
63 | ansi_normal()); | |
64 | ||
65 | return 0; | |
66 | } | |
67 | ||
68 | static int parse_argv(int argc, char *argv[]) { | |
69 | enum { | |
70 | ARG_VERSION = 0x100, | |
71 | ARG_BANK, | |
b0d00ec6 | 72 | ARG_PCR, |
708d7524 | 73 | ARG_TPM2_DEVICE, |
0318d545 | 74 | ARG_GRACEFUL, |
17984c55 LP |
75 | ARG_FILE_SYSTEM, |
76 | ARG_MACHINE_ID, | |
708d7524 LP |
77 | }; |
78 | ||
79 | static const struct option options[] = { | |
80 | { "help", no_argument, NULL, 'h' }, | |
81 | { "version", no_argument, NULL, ARG_VERSION }, | |
82 | { "bank", required_argument, NULL, ARG_BANK }, | |
b0d00ec6 | 83 | { "pcr", required_argument, NULL, ARG_PCR }, |
708d7524 | 84 | { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, |
0318d545 | 85 | { "graceful", no_argument, NULL, ARG_GRACEFUL }, |
17984c55 LP |
86 | { "file-system", required_argument, NULL, ARG_FILE_SYSTEM }, |
87 | { "machine-id", no_argument, NULL, ARG_MACHINE_ID }, | |
708d7524 LP |
88 | {} |
89 | }; | |
90 | ||
17984c55 | 91 | int c, r; |
708d7524 LP |
92 | |
93 | assert(argc >= 0); | |
94 | assert(argv); | |
95 | ||
96 | while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) | |
97 | switch (c) { | |
98 | ||
99 | case 'h': | |
100 | help(0, NULL, NULL); | |
101 | return 0; | |
102 | ||
103 | case ARG_VERSION: | |
104 | return version(); | |
105 | ||
106 | case ARG_BANK: { | |
107 | const EVP_MD *implementation; | |
108 | ||
109 | implementation = EVP_get_digestbyname(optarg); | |
110 | if (!implementation) | |
111 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg); | |
112 | ||
113 | if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0) | |
114 | return log_oom(); | |
115 | ||
116 | break; | |
117 | } | |
118 | ||
b0d00ec6 LP |
119 | case ARG_PCR: |
120 | r = tpm2_pcr_index_from_string(optarg); | |
121 | if (r < 0) | |
122 | return log_error_errno(r, "Failed to parse PCR index: %s", optarg); | |
123 | ||
124 | arg_pcr_index = r; | |
125 | break; | |
126 | ||
708d7524 LP |
127 | case ARG_TPM2_DEVICE: { |
128 | _cleanup_free_ char *device = NULL; | |
129 | ||
130 | if (streq(optarg, "list")) | |
131 | return tpm2_list_devices(); | |
132 | ||
133 | if (!streq(optarg, "auto")) { | |
134 | device = strdup(optarg); | |
135 | if (!device) | |
136 | return log_oom(); | |
137 | } | |
138 | ||
139 | free_and_replace(arg_tpm2_device, device); | |
140 | break; | |
141 | } | |
142 | ||
0318d545 LP |
143 | case ARG_GRACEFUL: |
144 | arg_graceful = true; | |
145 | break; | |
146 | ||
17984c55 LP |
147 | case ARG_FILE_SYSTEM: |
148 | r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_file_system); | |
149 | if (r < 0) | |
150 | return r; | |
151 | ||
152 | break; | |
153 | ||
154 | case ARG_MACHINE_ID: | |
155 | arg_machine_id = true; | |
156 | break; | |
157 | ||
708d7524 LP |
158 | case '?': |
159 | return -EINVAL; | |
160 | ||
161 | default: | |
162 | assert_not_reached(); | |
163 | } | |
164 | ||
17984c55 LP |
165 | if (arg_file_system && arg_machine_id) |
166 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system= and --machine-id may not be combined."); | |
167 | ||
4e16d5c6 LP |
168 | r = varlink_invocation(VARLINK_ALLOW_ACCEPT); |
169 | if (r < 0) | |
170 | return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); | |
171 | if (r > 0) | |
172 | arg_varlink = true; | |
173 | else if (arg_pcr_index == UINT_MAX) | |
b0d00ec6 LP |
174 | arg_pcr_index = (arg_file_system || arg_machine_id) ? |
175 | TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ | |
176 | TPM2_PCR_KERNEL_BOOT; /* → PCR 11 */ | |
177 | ||
708d7524 LP |
178 | return 1; |
179 | } | |
180 | ||
bd860983 | 181 | static int determine_banks(Tpm2Context *c, unsigned target_pcr_nr) { |
e4481cc5 LP |
182 | _cleanup_strv_free_ char **l = NULL; |
183 | int r; | |
708d7524 LP |
184 | |
185 | assert(c); | |
186 | ||
187 | if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */ | |
188 | return 0; | |
189 | ||
23e9ccc2 | 190 | r = tpm2_get_good_pcr_banks_strv(c, UINT32_C(1) << target_pcr_nr, &l); |
e4481cc5 | 191 | if (r < 0) |
f9a0ee75 | 192 | return log_error_errno(r, "Could not verify pcr banks: %m"); |
708d7524 | 193 | |
e4481cc5 | 194 | strv_free_and_replace(arg_banks, l); |
708d7524 LP |
195 | return 0; |
196 | } | |
197 | ||
4e16d5c6 LP |
198 | static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2UserspaceEventType event) { |
199 | _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; | |
200 | int r; | |
201 | ||
202 | r = tpm2_context_new(arg_tpm2_device, &c); | |
203 | if (r < 0) | |
204 | return r; | |
205 | ||
206 | r = determine_banks(c, pcr); | |
207 | if (r < 0) | |
208 | return r; | |
209 | if (strv_isempty(arg_banks)) /* Still none? */ | |
210 | return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate."); | |
211 | ||
212 | _cleanup_free_ char *joined_banks = NULL; | |
213 | joined_banks = strv_join(arg_banks, ", "); | |
214 | if (!joined_banks) | |
215 | return log_oom(); | |
216 | ||
217 | _cleanup_free_ char *safe = NULL; | |
218 | if (size > EXTENSION_STRING_SAFE_LIMIT) { | |
219 | safe = cescape_length(data, EXTENSION_STRING_SAFE_LIMIT); | |
220 | if (!safe) | |
221 | return log_oom(); | |
222 | ||
223 | if (!strextend(&safe, "...")) | |
224 | return log_oom(); | |
225 | } else { | |
226 | safe = cescape_length(data, size); | |
227 | if (!safe) | |
228 | return log_oom(); | |
229 | } | |
230 | ||
231 | log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks); | |
232 | ||
233 | r = tpm2_extend_bytes(c, arg_banks, pcr, data, size, /* secret= */ NULL, /* secret_size= */ 0, event, safe); | |
234 | if (r < 0) | |
235 | return log_error_errno(r, "Could not extend PCR: %m"); | |
236 | ||
237 | log_struct(LOG_INFO, | |
238 | "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, | |
239 | LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", pcr, safe, joined_banks), | |
240 | "MEASURING=%s", safe, | |
241 | "PCR=%u", pcr, | |
242 | "BANKS=%s", joined_banks); | |
243 | ||
244 | return 0; | |
245 | } | |
246 | ||
247 | typedef struct MethodExtendParameters { | |
248 | unsigned pcr; | |
249 | const char *text; | |
250 | void *data; | |
251 | size_t data_size; | |
252 | } MethodExtendParameters; | |
253 | ||
254 | static int json_dispatch_binary_data(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { | |
255 | MethodExtendParameters *p = ASSERT_PTR(userdata); | |
256 | _cleanup_free_ void *d = NULL; | |
257 | size_t l; | |
258 | int r; | |
259 | ||
260 | r = json_variant_unbase64(variant, &d, &l); | |
261 | if (r < 0) | |
262 | return json_log(variant, flags, r, "JSON variant is not a base64 string."); | |
263 | ||
264 | free_and_replace(p->data, d); | |
265 | p->data_size = l; | |
266 | ||
267 | return 0; | |
268 | } | |
269 | ||
270 | static int vl_method_extend(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { | |
271 | ||
272 | static const JsonDispatch dispatch_table[] = { | |
273 | { "pcr", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MethodExtendParameters, pcr), JSON_MANDATORY }, | |
274 | { "text", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, | |
275 | { "data", JSON_VARIANT_STRING, json_dispatch_binary_data, 0, 0 }, | |
276 | {} | |
277 | }; | |
278 | MethodExtendParameters p = { | |
279 | .pcr = UINT_MAX, | |
280 | }; | |
281 | int r; | |
282 | ||
283 | assert(link); | |
284 | ||
f1b622a0 LP |
285 | r = varlink_dispatch(link, parameters, dispatch_table, &p); |
286 | if (r != 0) | |
4e16d5c6 LP |
287 | return r; |
288 | ||
289 | if (!TPM2_PCR_INDEX_VALID(p.pcr)) | |
290 | return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "pcr"))); | |
291 | ||
292 | if (p.text) { | |
293 | /* Specifying both the text string and the binary data is not allowed */ | |
294 | if (p.data) | |
295 | return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "data"))); | |
296 | ||
297 | r = extend_now(p.pcr, p.text, strlen(p.text), _TPM2_USERSPACE_EVENT_TYPE_INVALID); | |
298 | } else if (p.data) | |
299 | r = extend_now(p.pcr, p.data, p.data_size, _TPM2_USERSPACE_EVENT_TYPE_INVALID); | |
300 | else | |
301 | return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "text"))); | |
302 | if (r < 0) | |
303 | return r; | |
304 | ||
305 | return varlink_reply(link, NULL); | |
306 | } | |
307 | ||
708d7524 | 308 | static int run(int argc, char *argv[]) { |
4e16d5c6 | 309 | _cleanup_free_ char *word = NULL; |
cb19bdae | 310 | Tpm2UserspaceEventType event; |
708d7524 LP |
311 | int r; |
312 | ||
313 | log_setup(); | |
314 | ||
315 | r = parse_argv(argc, argv); | |
316 | if (r <= 0) | |
317 | return r; | |
318 | ||
4e16d5c6 LP |
319 | if (arg_varlink) { |
320 | _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL; | |
321 | ||
322 | /* Invocation as Varlink service */ | |
323 | ||
324 | r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY); | |
325 | if (r < 0) | |
326 | return log_error_errno(r, "Failed to allocate Varlink server: %m"); | |
327 | ||
328 | r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_PCRExtend); | |
329 | if (r < 0) | |
330 | return log_error_errno(r, "Failed to add Varlink interface: %m"); | |
331 | ||
332 | r = varlink_server_bind_method(varlink_server, "io.systemd.PCRExtend.Extend", vl_method_extend); | |
333 | if (r < 0) | |
334 | return log_error_errno(r, "Failed to bind Varlink method: %m"); | |
335 | ||
336 | r = varlink_server_loop_auto(varlink_server); | |
337 | if (r < 0) | |
338 | return log_error_errno(r, "Failed to run Varlink event loop: %m"); | |
339 | ||
340 | return EXIT_SUCCESS; | |
341 | } | |
342 | ||
17984c55 | 343 | if (arg_file_system) { |
17984c55 LP |
344 | if (optind != argc) |
345 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument."); | |
708d7524 | 346 | |
4cdef9f0 | 347 | r = pcrextend_file_system_word(arg_file_system, &word, NULL); |
17984c55 | 348 | if (r < 0) |
4cdef9f0 | 349 | return r; |
17984c55 | 350 | |
cb19bdae | 351 | event = TPM2_EVENT_FILESYSTEM; |
17984c55 LP |
352 | |
353 | } else if (arg_machine_id) { | |
17984c55 LP |
354 | |
355 | if (optind != argc) | |
356 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument."); | |
357 | ||
4cdef9f0 | 358 | r = pcrextend_machine_id_word(&word); |
17984c55 | 359 | if (r < 0) |
4cdef9f0 | 360 | return r; |
17984c55 | 361 | |
cb19bdae | 362 | event = TPM2_EVENT_MACHINE_ID; |
17984c55 LP |
363 | |
364 | } else { | |
365 | if (optind+1 != argc) | |
366 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); | |
367 | ||
368 | word = strdup(argv[optind]); | |
369 | if (!word) | |
370 | return log_oom(); | |
371 | ||
372 | /* Refuse to measure an empty word. We want to be able to write the series of measured words | |
373 | * separated by colons, where multiple separating colons are collapsed. Thus it makes sense to | |
374 | * disallow an empty word to avoid ambiguities. */ | |
375 | if (isempty(word)) | |
376 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "String to measure cannot be empty, refusing."); | |
377 | ||
cb19bdae | 378 | event = TPM2_EVENT_PHASE; |
17984c55 | 379 | } |
708d7524 | 380 | |
0318d545 LP |
381 | if (arg_graceful && tpm2_support() != TPM2_SUPPORT_FULL) { |
382 | log_notice("No complete TPM2 support detected, exiting gracefully."); | |
383 | return EXIT_SUCCESS; | |
384 | } | |
385 | ||
708d7524 | 386 | /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */ |
be8f478c | 387 | r = efi_measured_uki(LOG_ERR); |
6c51b49c | 388 | if (r < 0) |
2f6c52b9 | 389 | return r; |
6c51b49c | 390 | if (r == 0) { |
2099cd62 | 391 | log_info("Kernel stub did not measure kernel image into PCR %i, skipping userspace measurement, too.", TPM2_PCR_KERNEL_BOOT); |
6c51b49c | 392 | return EXIT_SUCCESS; |
6337be0a | 393 | } |
708d7524 | 394 | |
4e16d5c6 | 395 | r = extend_now(arg_pcr_index, word, strlen(word), event); |
708d7524 | 396 | if (r < 0) |
f9a0ee75 | 397 | return log_error_errno(r, "Failed to create TPM2 context: %m"); |
708d7524 | 398 | |
708d7524 LP |
399 | return EXIT_SUCCESS; |
400 | } | |
401 | ||
402 | DEFINE_MAIN_FUNCTION(run); |