]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
measure: add 'sign' verb
authorLennart Poettering <lennart@poettering.net>
Wed, 17 Aug 2022 16:40:42 +0000 (18:40 +0200)
committerLennart Poettering <lennart@poettering.net>
Thu, 8 Sep 2022 14:27:07 +0000 (16:27 +0200)
man/systemd-measure.xml
src/boot/measure.c

index 65cb2e503e7da0347a6b75bb51c7ec63d6fbd0ab..0fc0d0e87d40a35e0c488b60276a313723a70157 100644 (file)
@@ -17,7 +17,7 @@
 
   <refnamediv>
     <refname>systemd-measure</refname>
-    <refpurpose>Pre-calculate expected TPM2 PCR values for booted unified kernel images</refpurpose>
+    <refpurpose>Pre-calculate and sign expected TPM2 PCR values for booted unified kernel images</refpurpose>
   </refnamediv>
 
   <refsynopsisdiv>
     <para>Note: this command is experimental for now. While it is likely to become a regular component of
     systemd, it might still change in behaviour and interface.</para>
 
-    <para><command>systemd-measure</command> is a tool that may be used to pre-calculate the expected TPM2
-    PCR 11 values that should be seen when a unified Linux kernel image based on
+    <para><command>systemd-measure</command> is a tool that may be used to pre-calculate and sign the
+    expected TPM2 PCR 11 values that should be seen when a unified Linux kernel image based on
     <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry> is
     booted up. It accepts paths to the ELF kernel image file, initial ram disk image file, devicetree file,
     kernel command line file,
     <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry> file, and
     boot splash file that make up the unified kernel image, and determines the PCR values expected to be in
     place after booting the image. Calculation starts with a zero-initialized PCR 11, and is executed in a
-    fashion compatible with what <filename>systemd-stub</filename> does at boot.</para>
+    fashion compatible with what <filename>systemd-stub</filename> does at boot. The result may optionally be
+    signed cryptographically, to allow TPM2 policies that can only be unlocked if a certain set of kernels is
+    booted, for which such a PCR signature can be provided.</para>
   </refsect1>
 
   <refsect1>
       <varlistentry>
         <term><command>calculate</command></term>
 
-        <listitem><para>Pre-calculate the expected value seen in PCR register 11 after boot-up of a unified
+        <listitem><para>Pre-calculate the expected values seen in PCR register 11 after boot-up of a unified
         kernel image consisting of the components specified with <option>--linux=</option>,
         <option>--osrel=</option>, <option>--cmdline=</option>, <option>--initrd=</option>,
         <option>--splash=</option>, <option>--dtb=</option>, see below. Only <option>--linux=</option> is
-        mandatory.</para></listitem>
+        mandatory. (Alternatively, specify <option>--current</option> to use the current values of PCR
+        register 11 instead.)</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>sign</command></term>
+
+        <listitem><para>As with the <command>calculate</command> command, pre-calculate the expected value
+        seen in TPM2 PCR register 11 after boot-up of a unified kernel image. Then, cryptographically sign
+        the resulting values with the private/public key pair (RSA) configured via
+        <option>--private-key=</option> and <option>--public-key=</option>. This will write a JSON object to
+        standard output that contains signatures for all specified PCR banks (see
+        <option>--pcr-bank=</option>) below, which may be used to unlock encrypted credentials (see
+        <citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>) or
+        LUKS volumes (see
+        <citerefentry><refentrytitle>systemd-cryptsetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>). This
+        allows binding secrets to a set of kernels for which such PCR 11 signatures can be provided.</para>
+
+        <para>Note that a TPM2 device must be available for this signing to take place, even though the
+        result is not tied to any TPM2 device or its state.</para></listitem>
       </varlistentry>
     </variablelist>
   </refsect1>
         <term><option>--splash=PATH</option></term>
         <term><option>--dtb=PATH</option></term>
 
-        <listitem><para>When used with the <command>calculate</command> verb, configures the files to read
-        the unified kernel image components from. Each option corresponds with the equally named section in
-        the unified kernel PE file. The <option>--linux=</option> switch expects the path to the ELF kernel
-        file that the unified PE kernel will wrap. All switches except <option>--linux=</option> are
-        optional. Each option may be used at most once.</para></listitem>
+        <listitem><para>When used with the <command>calculate</command> or <command>sign</command> verb,
+        configures the files to read the unified kernel image components from. Each option corresponds with
+        the equally named section in the unified kernel PE file. The <option>--linux=</option> switch expects
+        the path to the ELF kernel file that the unified PE kernel will wrap. All switches except
+        <option>--linux=</option> are optional. Each option may be used at most once.</para></listitem>
       </varlistentry>
 
       <varlistentry>
         <term><option>--current</option></term>
-        <listitem><para>When used with the <command>calculate</command> verb, takes the PCR 11 values
-        currently in effect for the system (which should typically reflect the hashes of the currently booted
-        kernel). This can be used in place of <option>--linux=</option> and the other switches listed
-        above.</para></listitem>
+        <listitem><para>When used with the <command>calculate</command> or <command>sign</command> verb,
+        takes the PCR 11 values currently in effect for the system (which should typically reflect the hashes
+        of the currently booted kernel). This can be used in place of <option>--linux=</option> and the other
+        switches listed above.</para></listitem>
       </varlistentry>
 
       <varlistentry>
         <term><option>--bank=DIGEST</option></term>
 
         <listitem><para>Controls the PCR banks to pre-calculate the PCR values for – in case
-        <command>calculate</command> is invoked –, or the banks to show in the <command>status</command>
-        output. May be used more then once to specify multiple banks. If not specified, defaults to the four
-        banks <literal>sha1</literal>, <literal>sha256</literal>, <literal>sha384</literal>,
-        <literal>sha512</literal>.</para></listitem>
+        <command>calculate</command> or <command>sign</command> is invoked –, or the banks to show in the
+        <command>status</command> output. May be used more then once to specify multiple banks. If not
+        specified, defaults to the four banks <literal>sha1</literal>, <literal>sha256</literal>,
+        <literal>sha384</literal>, <literal>sha512</literal>.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--private-key=PATH</option></term>
+        <term><option>--public-key=PATH</option></term>
+
+        <listitem><para>These switches take paths to a pair of PEM encoded RSA key files, for use with
+        the <command>sign</command> command.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--tpm2-device=</option><replaceable>PATH</replaceable></term>
+
+        <listitem><para>Controls which TPM2 device to use. Expects a device node path referring to the TPM2
+        chip (e.g. <filename>/dev/tpmrm0</filename>). Alternatively the special value <literal>auto</literal>
+        may be specified, in order to automatically determine the device node of a suitable TPM2 device (of
+        which there must be exactly one). The special value <literal>list</literal> may be used to enumerate
+        all suitable TPM2 devices currently discovered.</para></listitem>
       </varlistentry>
 
       <xi:include href="standard-options.xml" xpointer="json" />
     foo.efi
 # systemd-measure calculate \
      --linux=vmlinux \
-     --osrel=os-release \
+     --osrel=os-release.txt \
      --cmdline=cmdline.txt \
      --initrd=initrd.cpio \
      --splash=splash.bmp \
 11:sha512=8e79acd3ddbbc8282e98091849c3530f996303c8ac8e87a3b2378b71c8b3a6e86d5c4f41ecea9e1517090c3e8ec0c714821032038f525f744960bcd082d937da
 </programlisting>
     </example>
+
+    <example>
+      <title>Generate a private/public key pair, and a unified kernel image, and a TPM PCR 11 signature for it</title>
+
+      <programlisting># openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tpm2-pcr-private.pem
+# openssl rsa -pubout -in tpm2-pcr-private.pem -out tpm2-pcr-public.pem
+# objcopy \
+    --add-section .linux=vmlinux --change-section-vma .linux=0x2000000 \
+    --add-section .osrel=os-release.txt --change-section-vma .osrel=0x20000 \
+    --add-section .cmdline=cmdline.txt --change-section-vma .cmdline=0x30000 \
+    --add-section .initrd=initrd.cpio --change-section-vma .initrd=0x3000000 \
+    --add-section .splash=splash.bmp --change-section-vma .splash=0x100000 \
+    --add-section .dtb=devicetree.dtb --change-section-vma .dtb=0x40000 \
+    /usr/lib/systemd/boot/efi/linuxx64.efi.stub \
+    foo.efi
+# systemd-measure sign \
+     --linux=vmlinux \
+     --osrel=os-release.txt \
+     --cmdline=cmdline.txt \
+     --initrd=initrd.cpio \
+     --splash=splash.bmp \
+     --dtb=devicetree.dtb \
+     --bank=sha1 \
+     --bank=sha256 \
+     --private-key=tpm2-pcr-private.pem \
+     --public-key=tpm2-pcr-public.pem > tpm2-pcr-signature.json</programlisting>
+
+     <para>Later on, enroll the signed PCR policy on a LUKS volume:</para>
+
+     <programlisting># systemd-cryptenroll --tpm2-device=auto --tpm2-public-key=tpm2-pcr-public.pem --tpm2-signature=tpm2-pcr-signature.json /dev/sda5</programlisting>
+
+     <para>And then unlock the device with the signature:</para>
+
+     <programlisting># /usr/lib/systemd/systemd-cryptsetup attach myvolume /dev/sda5 - tpm2-device=auto,tpm2-signature=/path/to/tpm2-pcr-signature.json</programlisting>
+    </example>
   </refsect1>
 
   <refsect1>
     <para>
       <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
-      <citerefentry project='man-pages'><refentrytitle>objcopy</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+      <citerefentry project='man-pages'><refentrytitle>objcopy</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>systemd-cryptsetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
      </para>
   </refsect1>
 
index 33117fe939684f7556a07a41a9696615b5852aa5..33821688698450be305faa96c69654b4a6cb07d5 100644 (file)
@@ -14,6 +14,7 @@
 #include "parse-argument.h"
 #include "parse-util.h"
 #include "pretty-print.h"
+#include "sha256.h"
 #include "terminal-util.h"
 #include "tpm-pcr.h"
 #include "tpm2-util.h"
 
 static char *arg_sections[_UNIFIED_SECTION_MAX] = {};
 static char **arg_banks = NULL;
-static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static char *arg_tpm2_device = NULL;
+static char *arg_private_key = NULL;
+static char *arg_public_key = NULL;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_OFF;
 static PagerFlags arg_pager_flags = 0;
 static bool arg_current = false;
 
 STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_public_key, freep);
 
 static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) {
         for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++)
@@ -46,10 +53,11 @@ static int help(int argc, char *argv[], void *userdata) {
                 return log_oom();
 
         printf("%1$s  [OPTIONS...] COMMAND ...\n"
-               "\n%5$sPre-calculate PCR hash for kernel image.%6$s\n"
+               "\n%5$sPre-calculate and sign PCR hash for a unified kernel image.%6$s\n"
                "\n%3$sCommands:%4$s\n"
-               "  status             Show current PCR values\n"
-               "  calculate          Calculate expected PCR values\n"
+               "  status                 Show current PCR values\n"
+               "  calculate              Calculate expected PCR values\n"
+               "  sign                   Calculate and sign expected PCR values\n"
                "\n%3$sOptions:%4$s\n"
                "  -h --help              Show this help\n"
                "     --version           Print version\n"
@@ -62,6 +70,9 @@ static int help(int argc, char *argv[], void *userdata) {
                "     --dtb=PATH          Path to Devicetree file\n"
                "  -c --current           Use current PCR values\n"
                "     --bank=DIGEST       Select TPM bank (SHA1, SHA256)\n"
+               "     --tpm2-device=PATH  Use specified TPM2 device\n"
+               "     --private-key=KEY   Private key (PEM) to sign with\n"
+               "     --public-key=KEY    Public key (PEM) to validate against\n"
                "     --json=MODE         Output as JSON\n"
                "  -j                     Same as --json=pretty on tty, --json=short otherwise\n"
                "\nSee the %2$s for details.\n",
@@ -88,6 +99,9 @@ static int parse_argv(int argc, char *argv[]) {
                 _ARG_SECTION_LAST,
                 ARG_DTB = _ARG_SECTION_LAST,
                 ARG_BANK,
+                ARG_PRIVATE_KEY,
+                ARG_PUBLIC_KEY,
+                ARG_TPM2_DEVICE,
                 ARG_JSON,
         };
 
@@ -103,6 +117,9 @@ static int parse_argv(int argc, char *argv[]) {
                 { "dtb",         required_argument, NULL, ARG_DTB         },
                 { "current",     no_argument,       NULL, 'c'             },
                 { "bank",        required_argument, NULL, ARG_BANK        },
+                { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+                { "private-key", required_argument, NULL, ARG_PRIVATE_KEY },
+                { "public-key",  required_argument, NULL, ARG_PUBLIC_KEY  },
                 { "json",        required_argument, NULL, ARG_JSON        },
                 {}
         };
@@ -155,6 +172,36 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
+                case ARG_PRIVATE_KEY:
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_private_key);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_PUBLIC_KEY:
+                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_public_key);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_TPM2_DEVICE: {
+                        _cleanup_free_ char *device = NULL;
+
+                        if (streq(optarg, "list"))
+                                return tpm2_list_devices();
+
+                        if (!streq(optarg, "auto")) {
+                                device = strdup(optarg);
+                                if (!device)
+                                        return log_oom();
+                        }
+
+                        free_and_replace(arg_tpm2_device, device);
+                        break;
+                }
+
                 case 'j':
                         arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
                         break;
@@ -377,14 +424,9 @@ static int measure_pcr(PcrState *pcr_states, size_t n) {
         return 0;
 }
 
-static int verb_calculate(int argc, char *argv[], void *userdata) {
+static int pcr_states_allocate(PcrState **ret) {
         _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
-        _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
         size_t n = 0;
-        int r;
-
-        if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
 
         pcr_states = new0(PcrState, strv_length(arg_banks) + 1);
         if (!pcr_states)
@@ -419,6 +461,25 @@ static int verb_calculate(int argc, char *argv[], void *userdata) {
                 };
         }
 
+        *ret = TAKE_PTR(pcr_states);
+        return (int) n;
+}
+
+static int verb_calculate(int argc, char *argv[], void *userdata) {
+        _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+        _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+        size_t n;
+        int r;
+
+        if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
+
+        r = pcr_states_allocate(&pcr_states);
+        if (r < 0)
+                return r;
+
+        n = (size_t) r;
+
         r = measure_pcr(pcr_states, n);
         if (r < 0)
                 return r;
@@ -464,6 +525,235 @@ static int verb_calculate(int argc, char *argv[], void *userdata) {
         return 0;
 }
 
+static TPM2_ALG_ID convert_evp_md_name_to_tpm2_alg(const EVP_MD *md) {
+        const char *mdname;
+
+        mdname = EVP_MD_name(md);
+        if (strcaseeq(mdname, "sha1"))
+                return TPM2_ALG_SHA1;
+        if (strcaseeq(mdname, "sha256"))
+                return TPM2_ALG_SHA256;
+        if (strcaseeq(mdname, "sha384"))
+                return TPM2_ALG_SHA384;
+        if (strcaseeq(mdname, "sha512"))
+                return TPM2_ALG_SHA512;
+
+        return TPM2_ALG_ERROR;
+}
+
+static int verb_sign(int argc, char *argv[], void *userdata) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+        _cleanup_(EVP_PKEY_freep) EVP_PKEY *privkey = NULL, *pubkey = NULL;
+        _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+        _cleanup_fclose_ FILE *privkeyf = NULL , *pubkeyf = NULL;
+        ESYS_TR session_handle = ESYS_TR_NONE;
+        TSS2_RC rc;
+        size_t n;
+        int r;
+
+        if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
+
+        if (!arg_private_key)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key specified, use --private-key=.");
+        if (!arg_public_key)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No public key specified, use --public-key=.");
+
+        /* When signing we only support JSON output */
+        arg_json_format_flags &= ~JSON_FORMAT_OFF;
+
+        privkeyf = fopen(arg_private_key, "re");
+        if (!privkeyf)
+                return log_error_errno(errno, "Failed to open private key file '%s': %m", arg_private_key);
+
+        pubkeyf = fopen(arg_public_key, "re");
+        if (!pubkeyf)
+                return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key);
+
+        privkey = PEM_read_PrivateKey(privkeyf, NULL, NULL, NULL);
+        if (!privkey)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse private key '%s'.", arg_private_key);
+
+        pubkey = PEM_read_PUBKEY(pubkeyf, NULL, NULL, NULL);
+        if (!pubkey)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key);
+
+        r = pcr_states_allocate(&pcr_states);
+        if (r < 0)
+                return r;
+
+        n = (size_t) r;
+
+        r = measure_pcr(pcr_states, n);
+        if (r < 0)
+                return r;
+
+        r = dlopen_tpm2();
+        if (r < 0)
+                return r;
+
+        r = tpm2_context_init(arg_tpm2_device, &c);
+        if (r < 0)
+                return r;
+
+        for (size_t i = 0; i < n; i++) {
+                static const TPMT_SYM_DEF symmetric = {
+                        .algorithm = TPM2_ALG_AES,
+                        .keyBits.aes = 128,
+                        .mode.aes = TPM2_ALG_CFB,
+                };
+                PcrState *p = pcr_states + i;
+
+                rc = sym_Esys_StartAuthSession(
+                                c.esys_context,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                NULL,
+                                TPM2_SE_TRIAL,
+                                &symmetric,
+                                TPM2_ALG_SHA256,
+                                &session_handle);
+                if (rc != TSS2_RC_SUCCESS) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc));
+                        goto finish;
+                }
+
+                /* Generate a single hash value from the PCRs included in our policy. Given that that's
+                 * exactly one, the calculation is trivial. */
+                TPM2B_DIGEST intermediate_digest = {
+                        .size = SHA256_DIGEST_SIZE,
+                };
+                assert(sizeof(intermediate_digest.buffer) >= SHA256_DIGEST_SIZE);
+                sha256_direct(p->value, p->value_size, intermediate_digest.buffer);
+
+                TPM2_ALG_ID tpmalg = convert_evp_md_name_to_tpm2_alg(p->md);
+                if (tpmalg == TPM2_ALG_ERROR) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported PCR bank");
+                        goto finish;
+                }
+
+                TPML_PCR_SELECTION pcr_selection;
+                tpm2_pcr_mask_to_selection(1 << TPM_PCR_INDEX_KERNEL_IMAGE, tpmalg, &pcr_selection);
+
+                rc = sym_Esys_PolicyPCR(
+                                c.esys_context,
+                                session_handle,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                &intermediate_digest,
+                                &pcr_selection);
+                if (rc != TSS2_RC_SUCCESS) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to push PCR policy into TPM: %s", sym_Tss2_RC_Decode(rc));
+                        goto finish;
+                }
+
+                _cleanup_(Esys_Freep) TPM2B_DIGEST *pcr_policy_digest = NULL;
+                rc = sym_Esys_PolicyGetDigest(
+                                c.esys_context,
+                                session_handle,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                ESYS_TR_NONE,
+                                &pcr_policy_digest);
+                if (rc != TSS2_RC_SUCCESS) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc));
+                        goto finish;
+                }
+
+                session_handle = tpm2_flush_context_verbose(c.esys_context, session_handle);
+
+                _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX* mdctx = NULL;
+                mdctx = EVP_MD_CTX_new();
+                if (!mdctx) {
+                        r = log_oom();
+                        goto finish;
+                }
+
+                if (EVP_DigestSignInit(mdctx, NULL, p->md, NULL, privkey) != 1) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to initialize signature context.");
+                        goto finish;
+                }
+
+                if (EVP_DigestSignUpdate(mdctx, pcr_policy_digest->buffer, pcr_policy_digest->size) != 1) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to sign data.");
+                        goto finish;
+                }
+
+                size_t ss;
+                if (EVP_DigestSignFinal(mdctx, NULL, &ss) != 1) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to finalize signature");
+                        goto finish;
+                }
+
+                _cleanup_free_ void *sig = malloc(ss);
+                if (!ss) {
+                        r = log_oom();
+                        goto finish;
+                }
+
+                if (EVP_DigestSignFinal(mdctx, sig, &ss) != 1) {
+                        r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                            "Failed to acquire signature data");
+                        goto finish;
+                }
+
+                _cleanup_free_ void *pubkey_fp = NULL;
+                size_t pubkey_fp_size = 0;
+                r = pubkey_fingerprint(pubkey, EVP_sha256(), &pubkey_fp, &pubkey_fp_size);
+                if (r < 0)
+                        goto finish;
+
+                _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *a = NULL;
+
+                r = tpm2_make_pcr_json_array(UINT64_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &a);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to build JSON PCR mask array: %m");
+                        goto finish;
+                }
+
+                r = json_build(&bv, JSON_BUILD_ARRAY(
+                                               JSON_BUILD_OBJECT(
+                                                               JSON_BUILD_PAIR("pcrs", JSON_BUILD_VARIANT(a)),                                                   /* PCR mask */
+                                                               JSON_BUILD_PAIR("pkfp", JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)),                               /* SHA256 fingerprint of public key (DER) used for the signature */
+                                                               JSON_BUILD_PAIR("pol", JSON_BUILD_HEX(pcr_policy_digest->buffer, pcr_policy_digest->size)),       /* TPM2 policy hash that is signed */
+                                                               JSON_BUILD_PAIR("sig", JSON_BUILD_BASE64(sig, ss)))));                                            /* signature data */
+                if (r < 0) {
+                        log_error_errno(r, "Failed to build JSON object: %m");
+                        goto finish;
+                }
+
+                r = json_variant_set_field(&v, p->bank, bv);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to add JSON field: %m");
+                        goto finish;
+                }
+        }
+
+        if (!v)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unable to find a single working PCR bank.");
+
+        if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+                pager_open(arg_pager_flags);
+
+        json_variant_dump(v, arg_json_format_flags, stdout, NULL);
+        r = 0;
+
+finish:
+        session_handle = tpm2_flush_context_verbose(c.esys_context, session_handle);
+        return r;
+}
+
 static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) {
         _cleanup_free_ char *s = NULL;
         uint32_t v;
@@ -641,6 +931,7 @@ static int measure_main(int argc, char *argv[]) {
                 { "help",      VERB_ANY, VERB_ANY, 0,            help           },
                 { "status",    VERB_ANY, 1,        VERB_DEFAULT, verb_status    },
                 { "calculate", VERB_ANY, 1,        0,            verb_calculate },
+                { "sign",      VERB_ANY, 1,        0,            verb_sign      },
                 {}
         };