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 #