cryptenroll: hook up new TPM2 signed policies with cryptenroll

This commit is contained in:
Lennart Poettering 2022-08-17 17:29:44 +02:00
parent 75ddec9301
commit f0f4fcaeb7
7 changed files with 201 additions and 58 deletions

View file

@ -347,16 +347,52 @@
to <literal>no</literal>. Despite being called PIN, any character can be used, not just numbers.
</para>
<para>Note that incorrect PIN entry when unlocking increments the
TPM dictionary attack lockout mechanism, and may lock out users for a prolonged time, depending on
its configuration. The lockout mechanism is a global property of the TPM,
<command>systemd-cryptenroll</command> does not control or configure the lockout mechanism. You may
use tpm2-tss tools to inspect or configure the dictionary attack lockout, with
<citerefentry project='mankier'><refentrytitle>tpm2_getcap</refentrytitle><manvolnum>1</manvolnum></citerefentry> and
<citerefentry project='mankier'><refentrytitle>tpm2_dictionarylockout</refentrytitle><manvolnum>1</manvolnum></citerefentry>
<para>Note that incorrect PIN entry when unlocking increments the TPM dictionary attack lockout
mechanism, and may lock out users for a prolonged time, depending on its configuration. The lockout
mechanism is a global property of the TPM, <command>systemd-cryptenroll</command> does not control or
configure the lockout mechanism. You may use tpm2-tss tools to inspect or configure the dictionary
attack lockout, with <citerefentry
project='mankier'><refentrytitle>tpm2_getcap</refentrytitle><manvolnum>1</manvolnum></citerefentry>
and <citerefentry
project='mankier'><refentrytitle>tpm2_dictionarylockout</refentrytitle><manvolnum>1</manvolnum></citerefentry>
commands, respectively.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--tpm2-public-key=</option><arg>PATH</arg></term>
<term><option>--tpm2-public-key-pcrs=</option><arg rep="repeat">PCR</arg></term>
<term><option>--tpm2-signature=</option><arg>PATH</arg></term>
<listitem><para>Configures a TPM2 signed PCR policy to bind encryption to. The
<option>--tpm2-public-key=</option> option accepts a path to a PEM encoded RSA public key, to bind
the encryption to. If this is not specified explicitly, but a file
<filename>tpm2-pcr-public-key.pem</filename> exists in one of the directories
<filename>/etc/systemd/</filename>, <filename>/run/systemd/</filename>,
<filename>/usr/lib/systemd/</filename> (searched in this order), it is automatically used. The
<option>--tpm2-public-key-pcrs=</option> option takes a list of TPM2 PCR indexes to bind to (same
syntax as <option>--tpm2-pcrs=</option> described above). If not specified defaults to 11 (i.e. this
binds the policy to any unified kernel image for which a PCR signature can be provided).</para>
<para>Note the difference between <option>--tpm2-pcrs=</option> and
<option>--tpm2-public-key-pcrs=</option>: the former binds decryption to the current, specific PCR
values; the latter binds decryption to any set of PCR values for which a signature by the specified
public key can be provided. The latter is hence more useful in scenarios where software updates shell
be possible without losing access to all previously encrypted LUKS2 volumes.</para>
<para>The <option>--tpm2-signature=</option> option takes a path to a TPM2 PCR signature file
as generated by the
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>
tool. If this this is not specified explicitly a suitable signature file
<filename>tpm2-pcr-signature.json</filename> is searched for in <filename>/etc/systemd/</filename>,
<filename>/run/systemd/</filename>, <filename>/usr/lib/systemd/</filename> (in this order) and
used. If a signature file is specified or found it is used to verify if the volume can be unlocked
with it given the current PCR state, before the new slot is written to disk. This is intended as
safety net to ensure that access to a volume is not lost if a public key is enrolled for which no
valid signature for the current PCR state is available. If the supplied signature does not unlock the
current PCR state and public key combination, no slot is enrolled and the operation will fail. If no
signature file is specified or found no such safety verification is done.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--wipe-slot=</option><arg rep="repeat">SLOT</arg></term>
@ -411,7 +447,8 @@
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-cryptsetup@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>crypttab</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
<citerefentry project='die-net'><refentrytitle>cryptsetup</refentrytitle><manvolnum>8</manvolnum></citerefentry>
<citerefentry project='die-net'><refentrytitle>cryptsetup</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>
</para>
</refsect1>

View file

@ -4,6 +4,7 @@
#include "ask-password-api.h"
#include "cryptenroll-tpm2.h"
#include "env-util.h"
#include "fileio.h"
#include "hexdecoct.h"
#include "json.h"
#include "memory-util.h"
@ -131,13 +132,16 @@ int enroll_tpm2(struct crypt_device *cd,
size_t volume_key_size,
const char *device,
uint32_t hash_pcr_mask,
const char *pubkey_path,
uint32_t pubkey_pcr_mask,
const char *signature_path,
bool use_pin) {
_cleanup_(erase_and_freep) void *secret = NULL, *secret2 = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(erase_and_freep) void *secret = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *signature_json = NULL;
_cleanup_(erase_and_freep) char *base64_encoded = NULL;
size_t secret_size, secret2_size, blob_size, hash_size;
_cleanup_free_ void *blob = NULL, *hash = NULL;
size_t secret_size, blob_size, hash_size, pubkey_size = 0;
_cleanup_free_ void *blob = NULL, *hash = NULL, *pubkey = NULL;
uint16_t pcr_bank, primary_alg;
const char *node;
_cleanup_(erase_and_freep) char *pin_str = NULL;
@ -148,6 +152,7 @@ int enroll_tpm2(struct crypt_device *cd,
assert(volume_key);
assert(volume_key_size > 0);
assert(TPM2_PCR_MASK_VALID(hash_pcr_mask));
assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask));
assert_se(node = crypt_get_device_name(cd));
@ -157,10 +162,29 @@ int enroll_tpm2(struct crypt_device *cd,
return r;
}
r = tpm2_load_pcr_public_key(pubkey_path, &pubkey, &pubkey_size);
if (r < 0) {
if (pubkey_path || signature_path || r != -ENOENT)
return log_error_errno(r, "Failed read TPM PCR public key: %m");
log_debug_errno(r, "Failed to read TPM2 PCR public key, proceeding without: %m");
pubkey_pcr_mask = 0;
} else {
/* Also try to load the signature JSON object, to verify that our enrollment will work. This is optional however. */
r = tpm2_load_pcr_signature(signature_path, &signature_json);
if (r < 0) {
if (signature_path || r != -ENOENT)
return log_debug_errno(r, "Failed to read TPM PCR signature: %m");
log_debug_errno(r, "Failed to read TPM2 PCR signature, proceeding without: %m");
}
}
r = tpm2_seal(device,
hash_pcr_mask,
/* pubkey= */ NULL, /* pubkey_size= */ 0,
/* pubkey_pcr_mask= */ 0,
pubkey, pubkey_size,
pubkey_pcr_mask,
pin_str,
&secret, &secret_size,
&blob, &blob_size,
@ -181,24 +205,29 @@ int enroll_tpm2(struct crypt_device *cd,
return r; /* return existing keyslot, so that wiping won't kill it */
}
/* Quick verification that everything is in order, we are not in a hurry after all. */
log_debug("Unsealing for verification...");
r = tpm2_unseal(device,
hash_pcr_mask,
pcr_bank,
/* pubkey= */ NULL, /* pubkey_size= */ 0,
/* pubkey_pcr_mask= */ 0,
/* signature= */ NULL,
pin_str,
primary_alg,
blob, blob_size,
hash, hash_size,
&secret2, &secret2_size);
if (r < 0)
return r;
/* Quick verification that everything is in order, we are not in a hurry after all.*/
if (!pubkey || signature_json) {
_cleanup_(erase_and_freep) void *secret2 = NULL;
size_t secret2_size;
if (memcmp_nn(secret, secret_size, secret2, secret2_size) != 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 seal/unseal verification failed.");
log_debug("Unsealing for verification...");
r = tpm2_unseal(device,
hash_pcr_mask,
pcr_bank,
pubkey, pubkey_size,
pubkey_pcr_mask,
signature_json,
pin_str,
primary_alg,
blob, blob_size,
hash, hash_size,
&secret2, &secret2_size);
if (r < 0)
return r;
if (memcmp_nn(secret, secret_size, secret2, secret2_size) != 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 seal/unseal verification failed.");
}
/* let's base64 encode the key to use, for compat with homed (and it's easier to every type it in by keyboard, if that might end up being necessary. */
r = base64mem(secret, secret_size, &base64_encoded);
@ -219,7 +248,17 @@ int enroll_tpm2(struct crypt_device *cd,
if (keyslot < 0)
return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node);
r = tpm2_make_luks2_json(keyslot, hash_pcr_mask, pcr_bank, primary_alg, blob, blob_size, hash, hash_size, flags, &v);
r = tpm2_make_luks2_json(
keyslot,
hash_pcr_mask,
pcr_bank,
pubkey, pubkey_size,
pubkey_pcr_mask,
primary_alg,
blob, blob_size,
hash, hash_size,
flags,
&v);
if (r < 0)
return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");

View file

@ -7,9 +7,9 @@
#include "log.h"
#if HAVE_TPM2
int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t hash_pcr_mask, bool use_pin);
int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t hash_pcr_mask, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin);
#else
static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t pcr_mask, bool use_pin) {
static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t hash_pcr_mask, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin) {
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"TPM2 key enrollment not supported.");
}

View file

@ -26,6 +26,7 @@
#include "string-table.h"
#include "strv.h"
#include "terminal-util.h"
#include "tpm-pcr.h"
#include "tpm2-util.h"
static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID;
@ -35,6 +36,9 @@ static char *arg_fido2_device = NULL;
static char *arg_tpm2_device = NULL;
static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
static bool arg_tpm2_pin = false;
static char *arg_tpm2_public_key = NULL;
static uint32_t arg_tpm2_public_key_pcr_mask = UINT32_MAX;
static char *arg_tpm2_signature = NULL;
static char *arg_node = NULL;
static int *arg_wipe_slots = NULL;
static size_t arg_n_wipe_slots = 0;
@ -53,6 +57,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_unlock_keyfile, freep);
STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, freep);
STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep);
STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
STATIC_DESTRUCTOR_REGISTER(arg_wipe_slots, freep);
@ -114,6 +120,13 @@ static int help(void) {
" Enroll a TPM2 device\n"
" --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
" Specify TPM2 PCRs to seal against\n"
" --tpm2-public-key=PATH\n"
" Enroll signed TPM2 PCR policy against PEM public key\n"
" --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n"
" Enroll signed TPM2 PCR policy for specified TPM2 PCRs\n"
" --tpm2-signature=PATH\n"
" Validate public key enrollment works with JSON signature\n"
" file\n"
" --tpm2-with-pin=BOOL\n"
" Whether to require entering a PIN to unlock the volume\n"
" --wipe-slot=SLOT1,SLOT2,…\n"
@ -138,6 +151,9 @@ static int parse_argv(int argc, char *argv[]) {
ARG_FIDO2_DEVICE,
ARG_TPM2_DEVICE,
ARG_TPM2_PCRS,
ARG_TPM2_PUBLIC_KEY,
ARG_TPM2_PUBLIC_KEY_PCRS,
ARG_TPM2_SIGNATURE,
ARG_TPM2_PIN,
ARG_WIPE_SLOT,
ARG_FIDO2_WITH_PIN,
@ -147,21 +163,24 @@ static int parse_argv(int argc, char *argv[]) {
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "password", no_argument, NULL, ARG_PASSWORD },
{ "recovery-key", no_argument, NULL, ARG_RECOVERY_KEY },
{ "unlock-key-file", required_argument, NULL, ARG_UNLOCK_KEYFILE },
{ "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
{ "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG },
{ "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
{ "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN },
{ "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP },
{ "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV },
{ "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
{ "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS },
{ "tpm2-with-pin", required_argument, NULL, ARG_TPM2_PIN },
{ "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT },
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "password", no_argument, NULL, ARG_PASSWORD },
{ "recovery-key", no_argument, NULL, ARG_RECOVERY_KEY },
{ "unlock-key-file", required_argument, NULL, ARG_UNLOCK_KEYFILE },
{ "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
{ "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG },
{ "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
{ "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN },
{ "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP },
{ "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV },
{ "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
{ "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS },
{ "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY },
{ "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS },
{ "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE },
{ "tpm2-with-pin", required_argument, NULL, ARG_TPM2_PIN },
{ "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT },
{}
};
@ -329,6 +348,27 @@ static int parse_argv(int argc, char *argv[]) {
break;
case ARG_TPM2_PUBLIC_KEY:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key);
if (r < 0)
return r;
break;
case ARG_TPM2_PUBLIC_KEY_PCRS:
r = tpm2_parse_pcr_argument(optarg, &arg_tpm2_public_key_pcr_mask);
if (r < 0)
return r;
break;
case ARG_TPM2_SIGNATURE:
r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature);
if (r < 0)
return r;
break;
case ARG_WIPE_SLOT: {
const char *p = optarg;
@ -405,6 +445,8 @@ static int parse_argv(int argc, char *argv[]) {
if (arg_tpm2_pcr_mask == UINT32_MAX)
arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT;
if (arg_tpm2_public_key_pcr_mask == UINT32_MAX)
arg_tpm2_public_key_pcr_mask = UINT32_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE;
return 1;
}
@ -615,7 +657,7 @@ static int run(int argc, char *argv[]) {
break;
case ENROLL_TPM2:
slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask, arg_tpm2_pin);
slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin);
break;
case _ENROLL_TYPE_INVALID:

View file

@ -2950,7 +2950,17 @@ static int partition_encrypt(
if (keyslot < 0)
return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node);
r = tpm2_make_luks2_json(keyslot, arg_tpm2_pcr_mask, pcr_bank, primary_alg, blob, blob_size, hash, hash_size, 0, &v);
r = tpm2_make_luks2_json(
keyslot,
arg_tpm2_pcr_mask,
pcr_bank,
/* pubkey= */ NULL, /* pubkey_size= */ 0,
/* pubkey_pcr_mask= */ 0,
primary_alg,
blob, blob_size,
hash, hash_size,
0,
&v);
if (r < 0)
return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");

View file

@ -1822,8 +1822,11 @@ int tpm2_parse_pcr_json_array(JsonVariant *v, uint32_t *ret) {
int tpm2_make_luks2_json(
int keyslot,
uint32_t pcr_mask,
uint32_t hash_pcr_mask,
uint16_t pcr_bank,
const void *pubkey,
size_t pubkey_size,
uint32_t pubkey_pcr_mask,
uint16_t primary_alg,
const void *blob,
size_t blob_size,
@ -1832,31 +1835,43 @@ int tpm2_make_luks2_json(
TPM2Flags flags,
JsonVariant **ret) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *a = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *hmj = NULL, *pkmj = NULL;
_cleanup_free_ char *keyslot_as_string = NULL;
int r;
assert(blob || blob_size == 0);
assert(policy_hash || policy_hash_size == 0);
assert(pubkey || pubkey_size == 0);
if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
return -ENOMEM;
r = tpm2_make_pcr_json_array(pcr_mask, &a);
r = tpm2_make_pcr_json_array(hash_pcr_mask, &hmj);
if (r < 0)
return r;
if (pubkey_pcr_mask != 0) {
r = tpm2_make_pcr_json_array(pubkey_pcr_mask, &pkmj);
if (r < 0)
return r;
}
/* Note: We made the mistake of using "-" in the field names, which isn't particular compatible with
* other programming languages. Let's not make things worse though, i.e. future additions to the JSON
* object should use "_" rather than "-" in field names. */
r = json_build(&v,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("type", JSON_BUILD_CONST_STRING("systemd-tpm2")),
JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
JSON_BUILD_PAIR("tpm2-blob", JSON_BUILD_BASE64(blob, blob_size)),
JSON_BUILD_PAIR("tpm2-pcrs", JSON_BUILD_VARIANT(a)),
JSON_BUILD_PAIR("tpm2-pcrs", JSON_BUILD_VARIANT(hmj)),
JSON_BUILD_PAIR_CONDITION(!!tpm2_pcr_bank_to_string(pcr_bank), "tpm2-pcr-bank", JSON_BUILD_STRING(tpm2_pcr_bank_to_string(pcr_bank))),
JSON_BUILD_PAIR_CONDITION(!!tpm2_primary_alg_to_string(primary_alg), "tpm2-primary-alg", JSON_BUILD_STRING(tpm2_primary_alg_to_string(primary_alg))),
JSON_BUILD_PAIR("tpm2-policy-hash", JSON_BUILD_HEX(policy_hash, policy_hash_size)),
JSON_BUILD_PAIR("tpm2-pin", JSON_BUILD_BOOLEAN(flags & TPM2_FLAGS_USE_PIN)))
);
JSON_BUILD_PAIR("tpm2-pin", JSON_BUILD_BOOLEAN(flags & TPM2_FLAGS_USE_PIN)),
JSON_BUILD_PAIR_CONDITION(pubkey_pcr_mask != 0, "tpm2_pubkey_pcrs", JSON_BUILD_VARIANT(pkmj)),
JSON_BUILD_PAIR_CONDITION(pubkey_pcr_mask != 0, "tpm2_pubkey", JSON_BUILD_BASE64(pubkey, pubkey_size))));
if (r < 0)
return r;

View file

@ -81,7 +81,7 @@ int tpm2_parse_pcrs(const char *s, uint32_t *ret);
int tpm2_make_pcr_json_array(uint32_t pcr_mask, JsonVariant **ret);
int tpm2_parse_pcr_json_array(JsonVariant *v, uint32_t *ret);
int tpm2_make_luks2_json(int keyslot, uint32_t pcr_mask, uint16_t pcr_bank, uint16_t primary_alg, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, TPM2Flags flags, JsonVariant **ret);
int tpm2_make_luks2_json(int keyslot, uint32_t hash_pcr_mask, uint16_t pcr_bank, const void *pubkey, size_t pubkey_size, uint32_t pubkey_pcr_mask, uint16_t primary_alg, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, TPM2Flags flags, JsonVariant **ret);
#define TPM2_PCRS_MAX 24U