From 175cb87a1c1886440a9f14b8e3926dad47d1c117 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Sun, 19 Jan 2025 22:50:53 +0000 Subject: [PATCH] measure: add policy-digest verb When doing offline signing we need to know the exact payload to sign, and the 'calculate' verb doesn't really show that, it shows the PCR values. But what we sign is the hash of the policy. So add a new verb that outputs the json payload that goes in the .pcrsig section, without the .sig object, so that we can take them and give the .pol object to an offline and asynchronous signing service, such as SUSE's Open Build Service, and then add the .sig object to the json and attach it to a UKI. --- man/systemd-measure.xml | 14 ++++++++ src/measure/measure.c | 55 ++++++++++++++++++------------ test/units/TEST-70-TPM2.measure.sh | 25 +++++++++++--- 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index 97f1abe1f9..77a293ac2e 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -104,6 +104,20 @@ + + + policy-digest + + As with the sign command, pre-calculate the expected value + seen in TPM2 PCR register 11 after boot-up of a unified kernel image. Then, compute the resulting + TPM2 policy and print its digest. This will write a JSON object to standard output that contains + the policy digests for all specified PCR banks (see the option below), + so that it may be signed offline, for the cases where the private key is not directly accessible. + If or are specified, the JSON object + will also contain the key fingerprint. + + + diff --git a/src/measure/measure.c b/src/measure/measure.c index fceca7c4a1..dcae8528e0 100644 --- a/src/measure/measure.c +++ b/src/measure/measure.c @@ -77,6 +77,7 @@ static int help(int argc, char *argv[], void *userdata) { " status Show current PCR values\n" " calculate Calculate expected PCR values\n" " sign Calculate and sign expected PCR values\n" + " policy-digest Calculate expected TPM2 policy digests\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Print version\n" @@ -826,7 +827,7 @@ static int verb_calculate(int argc, char *argv[], void *userdata) { return 0; } -static int verb_sign(int argc, char *argv[], void *userdata) { +static int build_policy_digest(bool sign) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; @@ -839,7 +840,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing."); - if (!arg_private_key) + if (sign && !arg_private_key) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key specified, use --private-key=."); @@ -856,7 +857,7 @@ static int verb_sign(int argc, char *argv[], void *userdata) { "File '%s' is not a valid JSON object, refusing.", arg_append); } - /* When signing we only support JSON output */ + /* When signing/building digest we only support JSON output */ arg_json_format_flags &= ~SD_JSON_FORMAT_OFF; /* This must be done before openssl_load_private_key() otherwise it will get stuck */ @@ -918,11 +919,11 @@ static int verb_sign(int argc, char *argv[], void *userdata) { SYNTHETIC_ERRNO(EIO), "Failed to extract public key from certificate %s.", arg_certificate); - } else { + } else if (sign) { _cleanup_(memstream_done) MemStream m = {}; FILE *tf; - /* No public key was specified, let's derive it automatically, if we can */ + /* No public key was specified, let's derive it automatically, if we can, when signing */ tf = memstream_init(&m); if (!tf) @@ -978,17 +979,20 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return log_error_errno(r, "Could not calculate PolicyPCR digest: %m"); _cleanup_free_ void *sig = NULL; - size_t ss; - - r = digest_and_sign(p->md, privkey, pcr_policy_digest.buffer, pcr_policy_digest.size, &sig, &ss); - if (r < 0) - return log_error_errno(r, "Failed to sign PCR policy: %m"); + size_t ss = 0; + if (privkey) { + r = digest_and_sign(p->md, privkey, pcr_policy_digest.buffer, pcr_policy_digest.size, &sig, &ss); + if (r < 0) + return log_error_errno(r, "Failed to sign PCR policy: %m"); + } _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) - return r; + if (pubkey) { + r = pubkey_fingerprint(pubkey, EVP_sha256(), &pubkey_fp, &pubkey_fp_size); + if (r < 0) + return r; + } _cleanup_(sd_json_variant_unrefp) sd_json_variant *a = NULL; r = tpm2_make_pcr_json_array(UINT64_C(1) << TPM2_PCR_KERNEL_BOOT, &a); @@ -997,10 +1001,10 @@ static int verb_sign(int argc, char *argv[], void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *bv = NULL; r = sd_json_buildo(&bv, - SD_JSON_BUILD_PAIR("pcrs", SD_JSON_BUILD_VARIANT(a)), /* PCR mask */ - SD_JSON_BUILD_PAIR("pkfp", SD_JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */ - SD_JSON_BUILD_PAIR("pol", SD_JSON_BUILD_HEX(pcr_policy_digest.buffer, pcr_policy_digest.size)), /* TPM2 policy hash that is signed */ - SD_JSON_BUILD_PAIR("sig", SD_JSON_BUILD_BASE64(sig, ss))); /* signature data */ + SD_JSON_BUILD_PAIR("pcrs", SD_JSON_BUILD_VARIANT(a)), /* PCR mask */ + SD_JSON_BUILD_PAIR_CONDITION(pubkey_fp_size > 0, "pkfp", SD_JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */ + SD_JSON_BUILD_PAIR("pol", SD_JSON_BUILD_HEX(pcr_policy_digest.buffer, pcr_policy_digest.size)), /* TPM2 policy hash that is signed */ + SD_JSON_BUILD_PAIR_CONDITION(ss > 0, "sig", SD_JSON_BUILD_BASE64(sig, ss))); /* signature data */ if (r < 0) return log_error_errno(r, "Failed to build JSON object: %m"); @@ -1028,6 +1032,14 @@ static int verb_sign(int argc, char *argv[], void *userdata) { return 0; } +static int verb_sign(int argc, char *argv[], void *userdata) { + return build_policy_digest(/* sign= */ true); +} + +static int verb_policy_digest(int argc, char *argv[], void *userdata) { + return build_policy_digest(/* sign= */ false); +} + static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) { _cleanup_free_ char *s = NULL; uint32_t v; @@ -1179,10 +1191,11 @@ static int verb_status(int argc, char *argv[], void *userdata) { static int measure_main(int argc, char *argv[]) { static const Verb verbs[] = { - { "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 }, + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, + { "calculate", VERB_ANY, 1, 0, verb_calculate }, + { "policy-digest", VERB_ANY, 1, 0, verb_policy_digest }, + { "sign", VERB_ANY, 1, 0, verb_sign }, {} }; diff --git a/test/units/TEST-70-TPM2.measure.sh b/test/units/TEST-70-TPM2.measure.sh index 3336f38330..bf30bd57b3 100755 --- a/test/units/TEST-70-TPM2.measure.sh +++ b/test/units/TEST-70-TPM2.measure.sh @@ -47,15 +47,32 @@ EOF rm /tmp/result /tmp/result.json +# Generate key pair +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "/tmp/pcrsign-private.pem" +openssl rsa -pubout -in "/tmp/pcrsign-private.pem" -out "/tmp/pcrsign-public.pem" + +# Verify that the offline signature obtained via policy-digests is the same as an online signature created +# with the same key by systemd-measure +digest="$("$SD_MEASURE" policy-digest --json=short --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha256 --public-key="/tmp/pcrsign-public.pem" --phase=:)" +signed_digest="$("$SD_MEASURE" sign --json=short --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=:)" +echo "$digest" | jq -r '.sha256 | to_entries[] | .value.pol' | while read -r pol; do + # Note: basenc before coreutils 9.5 refuses lowercase hex input strings + offline_sig="$(echo -n "$pol" | \ + tr '[:lower:]' '[:upper:]' | \ + basenc --base16 --decode | \ + openssl dgst -sign /tmp/pcrsign-private.pem -sha256 | \ + base64 -w0)" + online_sig="$(echo "$signed_digest" | jq -r --arg pol "$pol" '.sha256[] | select(.pol == $pol) | .sig')" + test -n "$offline_sig" + test -n "$online_sig" + test "$offline_sig" = "$online_sig" +done + if ! tpm_has_pcr sha1 11 || ! tpm_has_pcr sha256 11; then echo "PCR sysfs files not found, skipping signed PCR policy tests" exit 0 fi -# Generate key pair -openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "/tmp/pcrsign-private.pem" -openssl rsa -pubout -in "/tmp/pcrsign-private.pem" -out "/tmp/pcrsign-public.pem" - MEASURE_BANKS=("--bank=sha256") # Check if SHA1 signatures are supported #