Coverage Report

Created: 2026-10-28 06:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/systemd/src/shared/pcrextend-util.c
Line
Count
Source
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3
#include "sd-device.h"
4
#include "sd-id128.h"
5
#include "sd-json.h"
6
#include "sd-varlink.h"
7
8
#include "alloc-util.h"
9
#include "blkid-util.h"
10
#include "blockdev-util.h"
11
#include "chase.h"
12
#include "errno-util.h"
13
#include "escape.h"
14
#include "fd-util.h"
15
#include "hexdecoct.h"
16
#include "id128-util.h"
17
#include "iovec-util.h"
18
#include "log.h"
19
#include "mountpoint-util.h"
20
#include "pcrextend-util.h"
21
#include "pkcs7-util.h"
22
#include "sha256.h"
23
#include "string-util.h"
24
#include "strv.h"
25
#include "tpm2-pcr.h"
26
#include "user-record.h"
27
28
0
int pcrextend_pcr_now(unsigned pcr, const char *word, const struct iovec *secret, const char *event) {
29
30
#if HAVE_TPM2
31
        int r;
32
33
        assert(word);
34
35
        if (!secret)
36
                secret = &iovec_empty;
37
38
        _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
39
        r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.PCRExtend");
40
        if (r < 0)
41
                return r;
42
43
        /* Build the parameters explicitly so that, when they carry the secret we can mark the
44
         * variant sensitive: this keeps the secret out of the debug log and ensures its heap
45
         * buffer is erased rather than merely freed. */
46
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *parameters = NULL;
47
        r = sd_json_buildo(
48
                        &parameters,
49
                        SD_JSON_BUILD_PAIR_INTEGER("pcr", pcr),
50
                        SD_JSON_BUILD_PAIR_STRING("text", word),
51
                        SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(secret),
52
                                                     "secret",
53
                                                     SD_JSON_BUILD_BASE64(secret->iov_base, secret->iov_len)),
54
                        SD_JSON_BUILD_PAIR_STRING("eventType", event));
55
        if (r < 0)
56
                return log_debug_errno(r, "Failed to build io.systemd.PCRExtend.Extend() parameters: %m");
57
58
        if (iovec_is_set(secret))
59
                sd_json_variant_sensitive(parameters);
60
61
        /* Both the reply and the error id are borrowed from the connection, don't unref/free them. */
62
        sd_json_variant *reply = NULL;
63
        const char *error_id = NULL;
64
        r = sd_varlink_call(vl, "io.systemd.PCRExtend.Extend", parameters, &reply, &error_id);
65
        if (r < 0)
66
                return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %m");
67
        if (error_id) {
68
                r = sd_varlink_error_to_errno(error_id, reply);
69
                if (r != -EBADR)
70
                        return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %m");
71
72
                return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %s", error_id);
73
        }
74
75
        log_debug("Measurement of '%s' into PCR %u completed.", word, pcr);
76
        return 1;
77
#else
78
0
        return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring.");
79
0
#endif
80
0
}
81
82
0
int pcrextend_nvpcr_now(const char *nvpcr, const char *word, const char *event) {
83
84
#if HAVE_TPM2
85
        int r;
86
87
        assert(nvpcr);
88
        assert(word);
89
90
        _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
91
        r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.PCRExtend");
92
        if (r < 0)
93
                return r;
94
95
        /* Both the reply and the error id are borrowed from the connection, don't unref/free them. */
96
        sd_json_variant *reply = NULL;
97
        const char *error_id = NULL;
98
        r = sd_varlink_callbo(
99
                        vl,
100
                        "io.systemd.PCRExtend.Extend",
101
                        &reply,
102
                        &error_id,
103
                        SD_JSON_BUILD_PAIR_STRING("nvpcr", nvpcr),
104
                        SD_JSON_BUILD_PAIR_STRING("text", word),
105
                        SD_JSON_BUILD_PAIR_STRING("eventType", event));
106
        if (r < 0)
107
                return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %m");
108
        if (error_id) {
109
                r = sd_varlink_error_to_errno(error_id, reply);
110
                if (r != -EBADR)
111
                        return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %m");
112
113
                return log_debug_errno(r, "Failed to issue io.systemd.PCRExtend.Extend() varlink call: %s", error_id);
114
        }
115
116
        log_debug("Measurement of '%s' into NvPCR '%s' completed.", word, nvpcr);
117
        return 1;
118
#else
119
0
        return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring.");
120
0
#endif
121
0
}
122
123
static int device_get_file_system_word(
124
                sd_device *d,
125
                const char *prefix,
126
0
                char **ret) {
127
128
#if HAVE_BLKID
129
        int r;
130
#endif
131
132
0
        assert(d);
133
0
        assert(prefix);
134
0
        assert(ret);
135
136
#if HAVE_BLKID
137
        r = dlopen_libblkid(LOG_DEBUG);
138
        if (r < 0)
139
                return r;
140
141
        _cleanup_close_ int block_fd = sd_device_open(d, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
142
        if (block_fd < 0)
143
                return block_fd;
144
145
        _cleanup_(blkid_free_probep) blkid_probe b = sym_blkid_new_probe();
146
        if (!b)
147
                return -ENOMEM;
148
149
        errno = 0;
150
        r = sym_blkid_probe_set_device(b, block_fd, 0, 0);
151
        if (r != 0)
152
                return errno_or_else(ENOMEM);
153
154
        (void) sym_blkid_probe_enable_superblocks(b, 1);
155
        (void) sym_blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID|BLKID_SUBLKS_LABEL);
156
        (void) sym_blkid_probe_enable_partitions(b, 1);
157
        (void) sym_blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS);
158
159
        errno = 0;
160
        r = sym_blkid_do_safeprobe(b);
161
        if (r == _BLKID_SAFEPROBE_ERROR)
162
                return errno_or_else(EIO);
163
        if (IN_SET(r, _BLKID_SAFEPROBE_AMBIGUOUS, _BLKID_SAFEPROBE_NOT_FOUND))
164
                return -ENOPKG;
165
166
        assert(r == _BLKID_SAFEPROBE_FOUND);
167
168
        _cleanup_strv_free_ char **l = strv_new(prefix);
169
        if (!l)
170
                return -ENOMEM;
171
172
        FOREACH_STRING(field, "TYPE", "UUID", "LABEL", "PART_ENTRY_UUID", "PART_ENTRY_TYPE", "PART_ENTRY_NAME") {
173
                const char *v = NULL;
174
175
                (void) sym_blkid_probe_lookup_value(b, field, &v, NULL);
176
177
                _cleanup_free_ char *escaped = xescape(strempty(v), ":"); /* Avoid ambiguity around ":" */
178
                if (!escaped)
179
                        return -ENOMEM;
180
181
                r = strv_consume(&l, TAKE_PTR(escaped));
182
                if (r < 0)
183
                        return r;
184
        }
185
186
        assert(strv_length(l) == 7); /* We always want 7 components, to avoid ambiguous strings */
187
188
        _cleanup_free_ char *word = strv_join(l, ":");
189
        if (!word)
190
                return -ENOMEM;
191
192
        *ret = TAKE_PTR(word);
193
        return 0;
194
#else
195
0
        return -EOPNOTSUPP;
196
0
#endif
197
0
}
198
199
0
int pcrextend_file_system_word(const char *path, char **ret_word, char **ret_normalized_path) {
200
0
        _cleanup_free_ char *normalized_path = NULL, *normalized_escaped = NULL, *prefix = NULL, *word = NULL;
201
0
        _cleanup_(sd_device_unrefp) sd_device *d = NULL;
202
0
        _cleanup_close_ int dfd = -EBADF;
203
0
        int r;
204
205
0
        assert(path);
206
0
        assert(ret_word);
207
208
0
        dfd = chase_and_open(path, NULL, 0, O_DIRECTORY|O_CLOEXEC, &normalized_path);
209
0
        if (dfd < 0)
210
0
                return log_error_errno(dfd, "Failed to open path '%s': %m", path);
211
212
0
        r = is_mount_point_at(dfd, NULL, 0);
213
0
        if (r < 0)
214
0
                return log_error_errno(r, "Failed to determine if path '%s' is mount point: %m", normalized_path);
215
0
        if (r == 0)
216
0
                return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Specified path '%s' is not a mount point, refusing.", normalized_path);
217
218
0
        normalized_escaped = xescape(normalized_path, ":"); /* Avoid ambiguity around ":" */
219
0
        if (!normalized_escaped)
220
0
                return log_oom();
221
222
0
        prefix = strjoin("file-system:", normalized_escaped);
223
0
        if (!prefix)
224
0
                return log_oom();
225
226
0
        r = block_device_new_from_fd(dfd, BLOCK_DEVICE_LOOKUP_BACKING, &d);
227
0
        if (r < 0) {
228
0
                log_notice_errno(r, "Unable to determine backing block device of '%s', using generic fallback file system identity string: %m", path);
229
230
0
                word = strjoin(prefix, "::::::");
231
0
                if (!word)
232
0
                        return log_oom();
233
0
        } else {
234
0
                r = device_get_file_system_word(d, prefix, &word);
235
0
                if (r < 0)
236
0
                        return log_error_errno(r, "Failed to get file system identifier string for '%s': %m", path);
237
0
        }
238
239
0
        *ret_word = TAKE_PTR(word);
240
241
0
        if (ret_normalized_path)
242
0
                *ret_normalized_path = TAKE_PTR(normalized_path);
243
244
0
        return 0;
245
0
}
246
247
0
int pcrextend_machine_id_word(char **ret) {
248
0
        _cleanup_free_ char *word = NULL;
249
0
        sd_id128_t mid;
250
0
        int r;
251
252
0
        assert(ret);
253
254
0
        r = sd_id128_get_machine(&mid);
255
0
        if (r < 0)
256
0
                return log_error_errno(r, "Failed to acquire machine ID: %m");
257
258
0
        word = strjoin("machine-id:", SD_ID128_TO_STRING(mid));
259
0
        if (!word)
260
0
                return log_oom();
261
262
0
        *ret = TAKE_PTR(word);
263
0
        return 0;
264
0
}
265
266
0
int pcrextend_product_id_word(char **ret) {
267
0
        _cleanup_free_ char *word = NULL;
268
0
        sd_id128_t pid;
269
0
        int r;
270
271
0
        assert(ret);
272
273
0
        r = id128_get_product(&pid);
274
0
        if (IN_SET(r, -ENOENT, -EADDRNOTAVAIL)) /* No product UUID field, or an all-zero or all-0xFF UUID */
275
0
                word = strdup("product-id:missing");
276
0
        else if (r < 0)
277
0
                return log_error_errno(r, "Failed to acquire product ID: %m");
278
0
        else
279
0
                word = strjoin("product-id:", SD_ID128_TO_STRING(pid));
280
0
        if (!word)
281
0
                return log_oom();
282
283
0
        *ret = TAKE_PTR(word);
284
0
        return 0;
285
0
}
286
287
0
int pcrextend_login_word(UserRecord *ur, char **ret) {
288
0
        int r;
289
290
0
        assert(ur);
291
0
        assert(ret);
292
293
        /* Reduce the user record to the sections that make up its stable, host-specific identity, and turn
294
         * that into a word to measure into the 'login' NvPCR. We deliberately keep the 'regular',
295
         * 'perMachine' and 'binding' sections (the portable identity plus how it is realized on *this*
296
         * host), but drop 'privileged' (so password hash churn doesn't perturb the NvPCR), 'secret' and
297
         * 'status' (transient) and 'signature' (so the scheme is identical for signed and unsigned records).
298
         * Only 'regular' is required, the others are merely allowed, so plain NSS users reduce cleanly. */
299
0
        UserRecordLoadFlags mask =
300
0
                USER_RECORD_REQUIRE_REGULAR |
301
0
                USER_RECORD_ALLOW_PER_MACHINE |
302
0
                USER_RECORD_ALLOW_BINDING |
303
0
                USER_RECORD_STRIP_PRIVILEGED |
304
0
                USER_RECORD_STRIP_SECRET |
305
0
                USER_RECORD_STRIP_STATUS |
306
0
                USER_RECORD_STRIP_SIGNATURE |
307
0
                USER_RECORD_PERMISSIVE;
308
309
0
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
310
0
        r = user_group_record_mangle(ur->json, mask, &v, /* ret_mask= */ NULL);
311
0
        if (r < 0)
312
0
                return log_error_errno(r, "Failed to reduce user record of '%s': %m", ur->user_name);
313
314
        /* Normalize so the serialization is canonical (stable key order) regardless of how the record was
315
         * assembled by the various userdb backends. */
316
0
        r = sd_json_variant_normalize(&v);
317
0
        if (r < 0)
318
0
                return log_error_errno(r, "Failed to normalize user record of '%s': %m", ur->user_name);
319
320
        /* Format to compact, single-line JSON (no SD_JSON_FORMAT_NEWLINE), so the measured word stays on one
321
         * line and the colon-separated word structure is unambiguous. */
322
0
        _cleanup_free_ char *text = NULL;
323
0
        r = sd_json_variant_format(v, /* flags= */ 0, &text);
324
0
        if (r < 0)
325
0
                return log_error_errno(r, "Failed to format user record of '%s': %m", ur->user_name);
326
327
0
        _cleanup_free_ char *name_escaped = xescape(ur->user_name, ":"); /* Avoid ambiguity around ":" */
328
0
        if (!name_escaped)
329
0
                return log_oom();
330
331
0
        _cleanup_free_ char *word = strjoin("login:", name_escaped, ":", text);
332
0
        if (!word)
333
0
                return log_oom();
334
335
0
        *ret = TAKE_PTR(word);
336
0
        return 0;
337
0
}
338
339
int pcrextend_verity_word(
340
                const char *name,
341
                const struct iovec *root_hash,
342
                const struct iovec *root_hash_sig,
343
0
                char **ret) {
344
345
0
        int r;
346
347
0
        assert(name);
348
0
        assert(iovec_is_set(root_hash));
349
0
        assert(ret);
350
351
0
        _cleanup_free_ char *name_escaped = xescape(name, ":"); /* Avoid ambiguity around ":" */
352
0
        if (!name_escaped)
353
0
                return log_oom();
354
355
0
        _cleanup_free_ char *h = hexmem(root_hash->iov_base, root_hash->iov_len);
356
0
        if (!h)
357
0
                return log_oom();
358
359
0
        _cleanup_free_ char *sigs = NULL;
360
0
        if (iovec_is_set(root_hash_sig)) {
361
0
                size_t n_signers = 0;
362
0
                Signer *signers = NULL;
363
364
                /* Let's extract the X.509 issuer + serial number from the PKCS#7 signature and include that
365
                 * in the measurement record. This is useful since it allows us to have different signing
366
                 * keys for confext + sysext + other types of DDIs, and by means of this information we can
367
                 * discern which kind it was. Ideally, we'd measure the fingerprint of the X.509 certificate,
368
                 * but typically that's not available in a PKCS#7 signature. */
369
370
0
                CLEANUP_ARRAY(signers, n_signers, signer_free_many);
371
372
0
                r = pkcs7_extract_signers(root_hash_sig, &signers, &n_signers);
373
0
                if (r < 0)
374
0
                        return r;
375
376
0
                FOREACH_ARRAY(i, signers, n_signers) {
377
0
                        _cleanup_free_ char *serial = hexmem(i->serial.iov_base, i->serial.iov_len);
378
0
                        if (!serial)
379
0
                                return log_oom();
380
381
0
                        _cleanup_free_ char *issuer = NULL;
382
0
                        if (base64mem(i->issuer.iov_base, i->issuer.iov_len, &issuer) < 0)
383
0
                                return log_oom();
384
385
0
                        if (strextendf_with_separator(&sigs, ",", "%s/%s", serial, issuer) < 0)
386
0
                                return log_oom();
387
0
                }
388
0
        }
389
390
0
        _cleanup_free_ char *word = strjoin("verity:", name_escaped, ":", h, ":", strempty(sigs));
391
0
        if (!word)
392
0
                return log_oom();
393
394
0
        *ret = TAKE_PTR(word);
395
0
        return 0;
396
0
}
397
398
int pcrextend_verity_now(
399
                const char *name,
400
                const struct iovec *root_hash,
401
0
                const struct iovec *root_hash_sig) {
402
403
0
        int r;
404
405
0
        _cleanup_free_ char *word = NULL;
406
0
        r = pcrextend_verity_word(
407
0
                        name,
408
0
                        root_hash,
409
0
                        root_hash_sig,
410
0
                        &word);
411
0
        if (r < 0)
412
0
                return r;
413
414
0
        return pcrextend_nvpcr_now("verity", word, "dm_verity");
415
0
}
416
417
#define IMDS_USERDATA_TRUNCATED_MAX 256U
418
419
0
int pcrextend_imds_userdata_word(const struct iovec *data, char **ret) {
420
0
        assert(iovec_is_set(data));
421
0
        assert(ret);
422
423
        /* We include both a hash of the complete user data, and a truncated version of the data in the word
424
         * we measure. The former protects the actual data, the latter is useful for debugging. */
425
426
0
        _cleanup_free_ char *hash = sha256_direct_hex(data->iov_base, data->iov_len);
427
0
        if (!hash)
428
0
                return log_oom();
429
430
0
        _cleanup_free_ char *data_encoded = NULL;
431
0
        if (base64mem_full(data->iov_base, MIN(data->iov_len, IMDS_USERDATA_TRUNCATED_MAX), /* line_break= */ SIZE_MAX, &data_encoded) < 0)
432
0
                return log_oom();
433
434
0
        _cleanup_free_ char *word = strjoin("imds-userdata:", hash, ":", data_encoded);
435
0
        if (!word)
436
0
                return log_oom();
437
438
0
        *ret = TAKE_PTR(word);
439
0
        return 0;
440
0
}
441
442
0
int pcrextend_imds_userdata_now(const struct iovec *data) {
443
0
        int r;
444
445
0
        _cleanup_free_ char *word = NULL;
446
0
        r = pcrextend_imds_userdata_word(data, &word);
447
0
        if (r < 0)
448
0
                return r;
449
450
0
        return pcrextend_pcr_now(TPM2_PCR_KERNEL_CONFIG, word, /* secret= */ NULL, "imds_userdata");
451
0
}