creds-util: permit credentials encrypted/signed by fixed zero length keys as fallback for systems lacking TPM2

This is supposed to be useful when generating credentials for immutable
initrd environments, where it is is relevant to support credentials even
on systems lacking a TPM2 chip.

With this, if `systemd-creds encrypt --with-key=auto-initrd` is used a
credential will be encrypted/signed with the TPM2 if it is available and
recognized by the firmware. Otherwise it will be encrypted/signed with
the fixed empty key, thus providing no confidentiality or authenticity.

The idea is that distributions use this mode to generically create
credentials that are as locked down as possible on the specific
platform.
This commit is contained in:
Lennart Poettering 2022-04-14 14:46:40 +02:00
parent 571d829ee4
commit b6553329c0
4 changed files with 87 additions and 21 deletions

View file

@ -263,23 +263,32 @@
<term><option>-H</option></term>
<term><option>-T</option></term>
<listitem><para>When specified with the <command>encrypt</command> command controls the encryption
key to use. Takes one of <literal>host</literal>, <literal>tpm2</literal>,
<literal>host+tpm2</literal> or <literal>auto</literal>. See above for details on the three key
types. If set to <literal>auto</literal> (which is the default) the TPM2 key is used if a TPM2 device
is found and not running in a container. The host key is used if
<filename>/var/lib/systemd/</filename> is on persistent media. This means on typical systems the
encryption is by default bound to both the TPM2 chip and the OS installation, and both need to be
available to decrypt the credential again. If <literal>auto</literal> is selected but neither TPM2 is
available (or running in container) nor <filename>/var/lib/systemd/</filename> is on persistent
media, encryption will fail.</para>
<listitem><para>When specified with the <command>encrypt</command> command controls the
encryption/signature key to use. Takes one of <literal>host</literal>, <literal>tpm2</literal>,
<literal>host+tpm2</literal>, <literal>tpm2-absent</literal>, <literal>auto</literal>,
<literal>auto-initrd</literal>. See above for details on the three key types. If set to
<literal>auto</literal> (which is the default) the TPM2 key is used if a TPM2 device is found and not
running in a container. The host key is used if <filename>/var/lib/systemd/</filename> is on
persistent media. This means on typical systems the encryption is by default bound to both the TPM2
chip and the OS installation, and both need to be available to decrypt the credential again. If
<literal>auto</literal> is selected but neither TPM2 is available (or running in container) nor
<filename>/var/lib/systemd/</filename> is on persistent media, encryption will fail. If set to
<literal>tpm2-absent</literal> a fixed zero length key is used (thus, in this mode no confidentiality
nor authenticity are provided!). This logic is useful to cover for systems that lack a TPM2 chip but
where credentials shall be generated. Note that decryption of such credentials is refused on systems
that have a TPM2 chip and where UEFI SecureBoot is enabled (this is done so that such a locked down
system cannot be tricked into loading a credential generated this way that lacks authentication
information). If set to <literal>auto-initrd</literal> a TPM2 key is used if a TPM2 is found. If not
a fixed zero length key is used, equivalent to <literal>tpm2-absent</literal> mode. This option is
particularly useful to generate credentials files that are encrypted/authenticated against TPM2 where
available but still work on systems lacking support for this.</para>
<para>The <option>-H</option> switch is a shortcut for <option>--with-key=host</option>. Similar,
<option>-T</option> is a shortcut for <option>--with-key=tpm2</option>.</para>
<para>When encrypting credentials that shall be used in the initial RAM disk (initrd) where
<filename>/var/lib/systemd/</filename> is typically not available make sure to use
<option>--with-key=tpm2</option> mode, to disable binding against the host secret.</para>
<option>--with-key=auto-initrd</option> mode, to disable binding against the host secret.</para>
<para>This switch has no effect on the <command>decrypt</command> command, as information on which
key to use for decryption is included in the encrypted credential already.</para></listitem>

View file

@ -560,7 +560,7 @@ static int verb_help(int argc, char **argv, void *userdata) {
" --timestamp=TIME Include specified timestamp in encrypted credential\n"
" --not-after=TIME Include specified invalidation time in encrypted\n"
" credential\n"
" --with-key=host|tpm2|host+tpm2|auto\n"
" --with-key=host|tpm2|host+tpm2|tpm2-absent|auto|auto-initrd\n"
" Which keys to encrypt with\n"
" -H Shortcut for --with-key=host\n"
" -T Shortcut for --with-key=tpm2\n"
@ -685,12 +685,16 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_WITH_KEY:
if (isempty(optarg) || streq(optarg, "auto"))
arg_with_key = _CRED_AUTO;
else if (streq(optarg, "auto-initrd"))
arg_with_key = _CRED_AUTO_INITRD;
else if (streq(optarg, "host"))
arg_with_key = CRED_AES256_GCM_BY_HOST;
else if (streq(optarg, "tpm2"))
arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC;
else if (STR_IN_SET(optarg, "host+tpm2", "tpm2+host"))
arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC;
else if (streq(optarg, "tpm2-absent"))
arg_with_key = CRED_AES256_GCM_BY_TPM2_ABSENT;
else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown key type: %s", optarg);

View file

@ -11,6 +11,7 @@
#include "blockdev-util.h"
#include "chattr-util.h"
#include "creds-util.h"
#include "efi-api.h"
#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
@ -366,6 +367,11 @@ int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *
*
* 3. The concatenation of the above.
*
* 4. Or a fixed "empty" key. This will not provide confidentiality or authenticity, of course, but is
* useful to encode credentials for the initrd on TPM-less systems, where we simply have no better
* concept to bind things to. Note that decryption of a key set up like this will be refused on
* systems that have a TPM and have SecureBoot enabled.
*
* The above is hashed with SHA256 which is then used as encryption key for AES256-GCM. The encrypted
* credential is a short (unencrypted) header describing which of the three keys to use, the IV to use for
* AES256-GCM and some more meta information (sizes of certain objects) that is strictly speaking redundant,
@ -482,9 +488,11 @@ int encrypt_credential_and_warn(
if (!sd_id128_in_set(with_key,
_CRED_AUTO,
_CRED_AUTO_INITRD,
CRED_AES256_GCM_BY_HOST,
CRED_AES256_GCM_BY_TPM2_HMAC,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC))
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC,
CRED_AES256_GCM_BY_TPM2_ABSENT))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid key type: " SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(with_key));
if (name && !credential_name_valid(name))
@ -534,6 +542,13 @@ int encrypt_credential_and_warn(
log_debug("Running in container, not attempting to use TPM2.");
try_tpm2 = r <= 0;
} else if (sd_id128_equal(with_key, _CRED_AUTO_INITRD)) {
/* If automatic mode for initrds is selected, we'll use the TPM2 key if the firmware does it,
* otherwise we'll use a fixed key */
try_tpm2 = efi_has_tpm2();
if (!try_tpm2)
log_debug("Firmware lacks TPM2 support, not attempting to use TPM2.");
} else
try_tpm2 = sd_id128_in_set(with_key, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
@ -550,7 +565,9 @@ int encrypt_credential_and_warn(
&tpm2_pcr_bank,
&tpm2_primary_alg);
if (r < 0) {
if (!sd_id128_equal(with_key, _CRED_AUTO))
if (sd_id128_equal(with_key, _CRED_AUTO_INITRD))
log_warning("Firmware reported a TPM2 being present and used, but we didn't manage to talk to it. Credential will be refused if SecureBoot is enabled.");
else if (!sd_id128_equal(with_key, _CRED_AUTO))
return r;
log_debug_errno(r, "TPM2 sealing didn't work, not using: %m");
@ -561,7 +578,7 @@ int encrypt_credential_and_warn(
}
#endif
if (sd_id128_equal(with_key, _CRED_AUTO)) {
if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD)) {
/* Let's settle the key type in auto mode now. */
if (host_key && tpm2_key)
@ -570,12 +587,17 @@ int encrypt_credential_and_warn(
id = CRED_AES256_GCM_BY_TPM2_HMAC;
else if (host_key)
id = CRED_AES256_GCM_BY_HOST;
else if (sd_id128_equal(with_key, _CRED_AUTO_INITRD))
id = CRED_AES256_GCM_BY_TPM2_ABSENT;
else
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
"TPM2 not available and host key located on temporary file system, no encryption key available.");
} else
id = with_key;
if (sd_id128_equal(id, CRED_AES256_GCM_BY_TPM2_ABSENT))
log_warning("Using a null key for encryption and signing. Confidentiality or authenticity will not be provided.");
/* Let's now take the host key and the TPM2 key and hash it together, to use as encryption key for the data */
r = sha256_hash_host_and_tpm2_key(host_key, host_key_size, tpm2_key, tpm2_key_size, md);
if (r < 0)
@ -733,7 +755,7 @@ int decrypt_credential_and_warn(
struct encrypted_credential_header *h;
struct metadata_credential_header *m;
uint8_t md[SHA256_DIGEST_LENGTH];
bool with_tpm2, with_host_key;
bool with_tpm2, with_host_key, is_tpm2_absent;
const EVP_CIPHER *cc;
int r, added;
@ -749,10 +771,31 @@ int decrypt_credential_and_warn(
with_host_key = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
with_tpm2 = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
is_tpm2_absent = sd_id128_equal(h->id, CRED_AES256_GCM_BY_TPM2_ABSENT);
if (!with_host_key && !with_tpm2)
if (!with_host_key && !with_tpm2 && !is_tpm2_absent)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unknown encryption format, or corrupted data: %m");
if (is_tpm2_absent) {
/* So this is a credential encrypted with a zero length key. We support this to cover for the
* case where neither a host key not a TPM2 are available (specifically: initrd environments
* where the host key is not yet accessible and no TPM2 chip exists at all), to minimize
* different codeflow for TPM2 and non-TPM2 codepaths. Of course, credentials encoded this
* way offer no confidentiality nor authenticity. Because of that it's important we refuse to
* use them on systems that actually *do* have a TPM2 chip if we are in SecureBoot
* mode. Otherwise an attacker could hand us credentials like this and we'd use them thinking
* they are trusted, even though they are not. */
if (efi_has_tpm2()) {
if (is_efi_secure_boot())
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG),
"Credential uses fixed key for fallback use when TPM2 is absent — but TPM2 is present, and SecureBoot is enabled, refusing.");
log_warning("Credential uses fixed key for use when TPM2 is absent, but TPM2 is present! Accepting anyway, since SecureBoot is disabled.");
} else
log_debug("Credential uses fixed key for use when TPM2 is absent, and TPM2 indeed is absent. Accepting.");
}
/* Now we know the minimum header size */
if (input_size < offsetof(struct encrypted_credential_header, iv))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
@ -833,6 +876,9 @@ int decrypt_credential_and_warn(
return log_error_errno(r, "Failed to determine local credential key: %m");
}
if (is_tpm2_absent)
log_warning("Warning: using a null key for decryption and authentication. Confidentiality or authenticity are not provided.");
sha256_hash_host_and_tpm2_key(host_key, host_key_size, tpm2_key, tpm2_key_size, md);
assert_se(cc = EVP_aes_256_gcm());

View file

@ -38,15 +38,22 @@ typedef enum CredentialSecretFlags {
int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *ret_size);
/* The three modes we support: keyed only by on-disk key, only by TPM2 HMAC key, and by the combination of both */
/* The four modes we support: keyed only by on-disk key, only by TPM2 HMAC key, and by the combination of
* both, as well as one with a fixed zero length key if TPM2 is missing (the latter of course provides no
* authenticity or confidentiality, but is still useful for integrity protection, and makes things simpler
* for us to handle). */
#define CRED_AES256_GCM_BY_HOST SD_ID128_MAKE(5a,1c,6a,86,df,9d,40,96,b1,d5,a6,5e,08,62,f1,9a)
#define CRED_AES256_GCM_BY_TPM2_HMAC SD_ID128_MAKE(0c,7c,c0,7b,11,76,45,91,9c,4b,0b,ea,08,bc,20,fe)
#define CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC SD_ID128_MAKE(93,a8,94,09,48,74,44,90,90,ca,f2,fc,93,ca,b5,53)
#define CRED_AES256_GCM_BY_TPM2_ABSENT SD_ID128_MAKE(05,84,69,da,f6,f5,43,24,80,05,49,da,0f,8e,a2,fb)
/* Special ID to pick automatic mode (i.e. tpm2+host if TPM2 exists, only host otherwise). This ID will never
* be stored on disk, but is useful only internally while figuring out what precisely to write to disk. To
* mark that this isn't a "real" type, we'll prefix it with an underscore. */
/* Two special IDs to pick a general automatic mode (i.e. tpm2+host if TPM2 exists, only host otherwise) or
* an initrd-specific automatic mode (i.e. tpm2 if firmware can do it, otherwise fixed zero-length key, and
* never involve host keys). These IDs will never be stored on disk, but are useful only internally while
* figuring out what precisely to write to disk. To mark that these aren't a "real" type, we'll prefix them
* with an underscore. */
#define _CRED_AUTO SD_ID128_MAKE(a2,19,cb,07,85,b2,4c,04,b1,6d,18,ca,b9,d2,ee,01)
#define _CRED_AUTO_INITRD SD_ID128_MAKE(02,dc,8e,de,3a,02,43,ab,a9,ec,54,9c,05,e6,a0,71)
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_pcr_mask, const void *input, size_t input_size, void **ret, size_t *ret_size);
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const void *input, size_t input_size, void **ret, size_t *ret_size);