diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml
index 4aa73d91f7..145d3f6188 100644
--- a/man/systemd-analyze.xml
+++ b/man/systemd-analyze.xml
@@ -168,6 +168,12 @@
image-policy
POLICY
+
+ systemd-analyze
+ OPTIONS
+ pcrs
+ PCR
+
@@ -883,6 +889,43 @@ var ignore - -
default ignore - -
+
+
+ systemd-analyze pcrs PCR…
+
+ This command shows the known TPM2 PCRs along with their identifying names and current values.
+
+
+ Example Output
+
+ $ systemd-analyze pcrs
+NR NAME SHA256
+ 0 platform-code bcd2eb527108bbb1f5528409bcbe310aa9b74f687854cc5857605993f3d9eb11
+ 1 platform-config b60622856eb7ce52637b80f30a520e6e87c347daa679f3335f4f1a600681bb01
+ 2 external-code 1471262403e9a62f9c392941300b4807fbdb6f0bfdd50abfab752732087017dd
+ 3 external-config 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
+ 4 boot-loader-code 939f7fa1458e1f7ce968874d908e524fc0debf890383d355e4ce347b7b78a95c
+ 5 boot-loader-config 864c61c5ea5ecbdb6951e6cb6d9c1f4b4eac79772f7fe13b8bece569d83d3768
+ 6 - 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
+ 7 secure-boot-policy 9c905bd9b9891bfb889b90a54c4b537b889cfa817c4389cc25754823a9443255
+ 8 - 0000000000000000000000000000000000000000000000000000000000000000
+ 9 kernel-initrd 9caa29b128113ef42aa53d421f03437be57211e5ebafc0fa8b5d4514ee37ff0c
+10 ima 5ea9e3dab53eb6b483b6ec9e3b2c712bea66bca1b155637841216e0094387400
+11 kernel-boot 0000000000000000000000000000000000000000000000000000000000000000
+12 kernel-config 627ffa4b405e911902fe1f1a8b0164693b31acab04f805f15bccfe2209c7eace
+13 sysexts 0000000000000000000000000000000000000000000000000000000000000000
+14 shim-policy 0000000000000000000000000000000000000000000000000000000000000000
+15 system-identity 0000000000000000000000000000000000000000000000000000000000000000
+16 debug 0000000000000000000000000000000000000000000000000000000000000000
+17 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+18 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+19 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+20 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+21 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+22 - ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+23 application-support 0000000000000000000000000000000000000000000000000000000000000000
+
+
diff --git a/src/analyze/analyze-pcrs.c b/src/analyze/analyze-pcrs.c
new file mode 100644
index 0000000000..b9a6ac144a
--- /dev/null
+++ b/src/analyze/analyze-pcrs.c
@@ -0,0 +1,144 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-pcrs.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "hexdecoct.h"
+#include "terminal-util.h"
+#include "tpm2-util.h"
+
+static int get_pcr_alg(const char **ret) {
+ assert(ret);
+
+ FOREACH_STRING(alg, "sha256", "sha1") {
+ _cleanup_free_ char *p = NULL;
+
+ if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/0", alg) < 0)
+ return log_oom();
+
+ if (access(p, F_OK) < 0) {
+ if (errno != -ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", p);
+ } else {
+ *ret = alg;
+ return 1;
+ }
+ }
+
+ log_notice("Kernel does not support reading PCR values.");
+ *ret = NULL;
+ return 0;
+}
+
+static int get_current_pcr(const char *alg, uint32_t pcr, void **ret, size_t *ret_size) {
+ _cleanup_free_ char *p = NULL, *s = NULL;
+ _cleanup_free_ void *buf = NULL;
+ size_t ss = 0, bufsize = 0;
+ int r;
+
+ assert(alg);
+ assert(ret);
+ assert(ret_size);
+
+ if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, alg, pcr) < 0)
+ return log_oom();
+
+ r = read_virtual_file(p, 4096, &s, &ss);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read '%s': %m", p);
+
+ r = unhexmem(s, ss, &buf, &bufsize);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode hex PCR data '%s': %m", s);
+
+ *ret = TAKE_PTR(buf);
+ *ret_size = bufsize;
+ return 0;
+}
+
+static int add_pcr_to_table(Table *table, const char *alg, uint32_t pcr) {
+ _cleanup_free_ char *h = NULL;
+ const char *color = NULL;
+ int r;
+
+ if (alg) {
+ _cleanup_free_ void *buf = NULL;
+ size_t bufsize = 0;
+
+ r = get_current_pcr(alg, pcr, &buf, &bufsize);
+ if (r < 0)
+ return r;
+
+ h = hexmem(buf, bufsize);
+ if (!h)
+ return log_oom();
+
+ /* Grey out PCRs that are not sensibly initialized */
+ if (memeqbyte(0, buf, bufsize) ||
+ memeqbyte(0xFFU, buf, bufsize))
+ color = ANSI_GREY;
+ }
+
+ r = table_add_many(table,
+ TABLE_UINT32, pcr,
+ TABLE_STRING, pcr_index_to_string(pcr),
+ TABLE_STRING, h,
+ TABLE_SET_COLOR, color);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+int verb_pcrs(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ const char *alg = NULL;
+ int r;
+
+ if (tpm2_support() != TPM2_SUPPORT_FULL)
+ log_notice("System has not TPM2 support, not showing PCR state.");
+ else {
+ r = get_pcr_alg(&alg);
+ if (r < 0)
+ return r;
+ }
+
+ table = table_new("nr", "name", alg);
+ if (!table)
+ return log_oom();
+
+ (void) table_set_align_percent(table, table_get_cell(table, 0, 0), 100);
+ (void) table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ if (!alg) /* hide hash column if we couldn't acquire it */
+ (void) table_set_display(table, 0, 1);
+
+ if (strv_isempty(strv_skip(argv, 1)))
+ for (uint32_t pi = 0; pi < _PCR_INDEX_MAX_DEFINED; pi++) {
+ r = add_pcr_to_table(table, alg, pi);
+ if (r < 0)
+ return r;
+ }
+ else {
+ for (int i = 1; i < argc; i++) {
+ int pi;
+
+ pi = pcr_index_from_string(argv[i]);
+ if (pi < 0)
+ return log_error_errno(pi, "PCR index \"%s\" not known.", argv[i]);
+
+ r = add_pcr_to_table(table, alg, pi);
+ if (r < 0)
+ return r;
+ }
+
+ (void) table_set_sort(table, (size_t) 0);
+ }
+
+ r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to output table: %m");
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-pcrs.h b/src/analyze/analyze-pcrs.h
new file mode 100644
index 0000000000..2a59511885
--- /dev/null
+++ b/src/analyze/analyze-pcrs.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_pcrs(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c
index b555c713fc..eb5fceba8d 100644
--- a/src/analyze/analyze.c
+++ b/src/analyze/analyze.c
@@ -28,6 +28,7 @@
#include "analyze-inspect-elf.h"
#include "analyze-log-control.h"
#include "analyze-malloc.h"
+#include "analyze-pcrs.h"
#include "analyze-plot.h"
#include "analyze-security.h"
#include "analyze-service-watchdogs.h"
@@ -234,6 +235,7 @@ static int help(int argc, char *argv[], void *userdata) {
" inspect-elf FILE... Parse and print ELF package metadata\n"
" malloc [D-BUS SERVICE...] Dump malloc stats of a D-Bus service\n"
" fdstore SERVICE... Show file descriptor store contents of service\n"
+ " pcrs [PCR...] Show TPM2 PCRs and their names\n"
"\nOptions:\n"
" --recursive-errors=MODE Control which units are verified\n"
" --offline=BOOL Perform a security review on unit file(s)\n"
@@ -545,9 +547,9 @@ static int parse_argv(int argc, char *argv[]) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Option --offline= is only supported for security right now.");
- if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf", "plot", "fdstore"))
+ if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf", "plot", "fdstore", "pcrs"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Option --json= is only supported for security, inspect-elf, plot, and fdstore right now.");
+ "Option --json= is only supported for security, inspect-elf, plot, fdstore, pcrs right now.");
if (arg_threshold != 100 && !streq_ptr(argv[optind], "security"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
@@ -636,6 +638,7 @@ static int run(int argc, char *argv[]) {
{ "malloc", VERB_ANY, VERB_ANY, 0, verb_malloc },
{ "fdstore", 2, VERB_ANY, 0, verb_fdstore },
{ "image-policy", 2, 2, 0, verb_image_policy },
+ { "pcrs", VERB_ANY, VERB_ANY, 0, verb_pcrs },
{}
};
diff --git a/src/analyze/meson.build b/src/analyze/meson.build
index c50c35f09f..911c0bc3ed 100644
--- a/src/analyze/meson.build
+++ b/src/analyze/meson.build
@@ -17,6 +17,7 @@ systemd_analyze_sources = files(
'analyze-inspect-elf.c',
'analyze-log-control.c',
'analyze-malloc.c',
+ 'analyze-pcrs.c',
'analyze-plot.c',
'analyze-security.c',
'analyze-service-watchdogs.c',
diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c
index 01a7a27ff3..bbff2a934b 100644
--- a/src/shared/tpm2-util.c
+++ b/src/shared/tpm2-util.c
@@ -4046,3 +4046,4 @@ static const char* const pcr_index_table[_PCR_INDEX_MAX_DEFINED] = {
};
DEFINE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_FALLBACK(pcr_index, int, TPM2_PCRS_MAX - 1);
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(pcr_index, int);
diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h
index 953e936c75..ea17b26b30 100644
--- a/src/shared/tpm2-util.h
+++ b/src/shared/tpm2-util.h
@@ -221,4 +221,5 @@ int tpm2_util_pbkdf2_hmac_sha256(const void *pass,
size_t saltlen,
uint8_t res[static SHA256_DIGEST_SIZE]);
-int pcr_index_from_string(const char *s);
+int pcr_index_from_string(const char *s) _pure_;
+const char *pcr_index_to_string(int pcr) _const_;
diff --git a/test/units/testsuite-65.sh b/test/units/testsuite-65.sh
index c6a81b4b4e..4c8c4146ed 100755
--- a/test/units/testsuite-65.sh
+++ b/test/units/testsuite-65.sh
@@ -838,6 +838,11 @@ systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -e '^usr
(! systemd-analyze image-policy 'doedel')
+# Output is very hard to predict, but let's run it for coverage anyway
+systemd-analyze pcrs
+systemd-analyze pcrs --json=pretty
+systemd-analyze pcrs 14 7 0 ima
+
systemd-analyze log-level info
echo OK >/testok