1
0
mirror of https://github.com/systemd/systemd.git synced 2025-01-26 14:04:03 +03:00

Merge pull request #23084 from poettering/creds-no-tpm2-fallback

creds: add semi-automatic fallback support for initrd credentials on systems lacking TPM2
This commit is contained in:
Yu Watanabe 2022-04-21 04:11:19 +09:00 committed by GitHub
commit f4bdbae725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 36 deletions

6
TODO
View File

@ -114,12 +114,6 @@ Features:
- sd-stub: automatically pick up microcode from ESP (/loader/microcode/*)
and synthesize initrd from it, and measure it. Signing is not necessary, as
microcode does that on its own. Pass as first initrd to kernel.
- systemd-creds should have a fallback logic that uses neither TPM nor the
system key in /var for encryption and instead some fixed key. This should
be opt in (since it provides no security properties) but be used by
kernel-install when encrypting the creds it generates on systems that lack
a TPM, so that we can have very similar codepaths on TPM and TPM-less
systems. i.e. --with-key=tpm-graceful or so.
- sd-stub should measure the kernel/initrd/… into a separate PCR, so that we
have one PCR we can bind the encrypted creds to that is not effected by
anything else but what we drop in via kernel-install, i.e. by earlier EFI

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

@ -40,7 +40,7 @@ static bool arg_legend = true;
static bool arg_system = false;
static TranscodeMode arg_transcode = TRANSCODE_OFF;
static int arg_newline = -1;
static sd_id128_t arg_with_key = SD_ID128_NULL;
static sd_id128_t arg_with_key = _CRED_AUTO;
static const char *arg_tpm2_device = NULL;
static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
static const char *arg_name = NULL;
@ -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"
@ -684,13 +684,17 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_WITH_KEY:
if (isempty(optarg) || streq(optarg, "auto"))
arg_with_key = SD_ID128_NULL;
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,
@ -474,15 +480,21 @@ int encrypt_credential_and_warn(
int ksz, bsz, ivsz, tsz, added, r;
uint8_t md[SHA256_DIGEST_LENGTH];
const EVP_CIPHER *cc;
#if HAVE_TPM2
bool try_tpm2 = false;
#endif
sd_id128_t id;
assert(input || input_size == 0);
assert(ret);
assert(ret_size);
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_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))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name: %s", name);
@ -500,23 +512,26 @@ int encrypt_credential_and_warn(
log_debug("Including not-after timestamp '%s' in encrypted credential.", format_timestamp(buf, sizeof(buf), not_after));
}
if (sd_id128_is_null(with_key) ||
sd_id128_in_set(with_key, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC)) {
if (sd_id128_in_set(with_key,
_CRED_AUTO,
CRED_AES256_GCM_BY_HOST,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC)) {
r = get_credential_host_secret(
CREDENTIAL_SECRET_GENERATE|
CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED|
(sd_id128_is_null(with_key) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0),
(sd_id128_equal(with_key, _CRED_AUTO) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0),
&host_key,
&host_key_size);
if (r == -ENOMEDIUM && sd_id128_is_null(with_key))
if (r == -ENOMEDIUM && sd_id128_equal(with_key, _CRED_AUTO))
log_debug_errno(r, "Credential host secret location on temporary file system, not using.");
else if (r < 0)
return log_error_errno(r, "Failed to determine local credential host secret: %m");
}
#if HAVE_TPM2
if (sd_id128_is_null(with_key)) {
bool try_tpm2;
if (sd_id128_equal(with_key, _CRED_AUTO)) {
/* If automatic mode is selected and we are running in a container, let's not try TPM2. OTOH
* if user picks TPM2 explicitly, let's always honour the request and try. */
@ -527,11 +542,17 @@ 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 */
if (try_tpm2 ||
sd_id128_in_set(with_key, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC)) {
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);
if (try_tpm2) {
r = tpm2_seal(tpm2_device,
tpm2_pcr_mask,
NULL,
@ -544,7 +565,9 @@ int encrypt_credential_and_warn(
&tpm2_pcr_bank,
&tpm2_primary_alg);
if (r < 0) {
if (!sd_id128_is_null(with_key))
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");
@ -555,7 +578,7 @@ int encrypt_credential_and_warn(
}
#endif
if (sd_id128_is_null(with_key)) {
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)
@ -564,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)
@ -727,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;
@ -743,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.");
@ -827,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,10 +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)
/* 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);