<varlistentry>
<term><option>--allow-null</option></term>
- <listitem><para>Allow decrypting credentials that use an empty key.</para>
+ <listitem><para>Allow decrypting credentials that use a null key. By default decryption of credentials encrypted/authenticated with a null key is only allowed if UEFI SecureBoot is off.</para>
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--refuse-null</option></term>
+
+ <listitem><para>Refuse decrypting credentials that use a null key, regardless of the UEFI SecureBoot state (see above).</para>
+
+ <xi:include href="version-info.xml" xpointer="v259"/></listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>--quiet</option></term>
<term><option>-q</option></term>
static bool arg_quiet = false;
static bool arg_varlink = false;
static uid_t arg_uid = UID_INVALID;
-static bool arg_allow_null = false;
+static CredentialFlags arg_credential_flags = 0;
static bool arg_ask_password = true;
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
timestamp,
uid_is_valid(arg_uid) ? arg_uid : getuid(),
&IOVEC_MAKE(data, size),
- CREDENTIAL_ANY_SCOPE,
+ arg_credential_flags | CREDENTIAL_ANY_SCOPE,
&plaintext);
else
r = decrypt_credential_and_warn(
arg_tpm2_signature,
uid_is_valid(arg_uid) ? arg_uid : getuid(),
&IOVEC_MAKE(data, size),
- CREDENTIAL_ANY_SCOPE,
+ arg_credential_flags | CREDENTIAL_ANY_SCOPE,
&plaintext);
if (r < 0)
return r;
arg_not_after,
arg_uid,
&plaintext,
- arg_ask_password ? CREDENTIAL_IPC_ALLOW_INTERACTIVE : 0,
+ arg_credential_flags,
&output);
} else
r = encrypt_credential_and_warn(
arg_tpm2_public_key_pcr_mask,
arg_uid,
&plaintext,
- /* flags= */ 0,
+ arg_credential_flags,
&output);
if (r < 0)
return r;
timestamp,
arg_uid,
&input,
- arg_ask_password ? CREDENTIAL_IPC_ALLOW_INTERACTIVE : 0,
+ arg_credential_flags,
&plaintext);
} else
r = decrypt_credential_and_warn(
arg_tpm2_signature,
arg_uid,
&input,
- arg_allow_null ? CREDENTIAL_ALLOW_NULL : 0,
+ arg_credential_flags,
&plaintext);
if (r < 0)
return r;
" Specify signature for public key PCR policy\n"
" --user Select user-scoped credential encryption\n"
" --uid=UID Select user for scoped credentials\n"
- " --allow-null Allow decrypting credentials with empty key\n"
+ " --allow-null Allow decrypting credentials with null key\n"
+ " --refuse-null Refuse decrypting credentials with null key\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
ARG_USER,
ARG_UID,
ARG_ALLOW_NULL,
+ ARG_REFUSE_NULL,
ARG_NO_ASK_PASSWORD,
};
{ "user", no_argument, NULL, ARG_USER },
{ "uid", required_argument, NULL, ARG_UID },
{ "allow-null", no_argument, NULL, ARG_ALLOW_NULL },
+ { "refuse-null", no_argument, NULL, ARG_REFUSE_NULL },
{ "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
{}
};
break;
case ARG_ALLOW_NULL:
- arg_allow_null = true;
+ arg_credential_flags &= ~CREDENTIAL_REFUSE_NULL;
+ arg_credential_flags |= CREDENTIAL_ALLOW_NULL;
+ break;
+
+ case ARG_REFUSE_NULL:
+ arg_credential_flags |= CREDENTIAL_REFUSE_NULL;
+ arg_credential_flags &= ~CREDENTIAL_ALLOW_NULL;
break;
case ARG_NO_ASK_PASSWORD:
}
}
+ SET_FLAG(arg_credential_flags, CREDENTIAL_IPC_ALLOW_INTERACTIVE, arg_ask_password);
+
if (uid_is_valid(arg_uid)) {
/* If a UID is specified, then switch to scoped credentials */
uint64_t timestamp;
CredentialScope scope;
uid_t uid;
+ int allow_null;
} MethodDecryptParameters;
static void method_decrypt_parameters_done(MethodDecryptParameters *p) {
static int vl_method_decrypt(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
static const sd_json_dispatch_field dispatch_table[] = {
- { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodDecryptParameters, name), 0 },
- { "blob", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodDecryptParameters, blob), SD_JSON_MANDATORY },
- { "timestamp", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(MethodDecryptParameters, timestamp), 0 },
- { "scope", SD_JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodDecryptParameters, scope), 0 },
- { "uid", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(MethodDecryptParameters, uid), 0 },
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodDecryptParameters, name), 0 },
+ { "blob", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodDecryptParameters, blob), SD_JSON_MANDATORY },
+ { "timestamp", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(MethodDecryptParameters, timestamp), 0 },
+ { "scope", SD_JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodDecryptParameters, scope), 0 },
+ { "uid", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(MethodDecryptParameters, uid), 0 },
+ { "allowNull", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodDecryptParameters, allow_null), SD_JSON_NULLABLE },
VARLINK_DISPATCH_POLKIT_FIELD,
{}
};
.timestamp = UINT64_MAX,
.scope = _CREDENTIAL_SCOPE_INVALID,
.uid = UID_INVALID,
+ .allow_null = -1,
};
bool timestamp_fresh, any_scope_after_polkit = false;
_cleanup_(iovec_done_erase) struct iovec output = {};
if (r < 0)
return r;
+ if (p.allow_null > 0)
+ cflags |= CREDENTIAL_ALLOW_NULL;
+ else if (p.allow_null == 0)
+ cflags |= CREDENTIAL_REFUSE_NULL;
+
r = sd_varlink_get_peer_uid(link, &peer_uid);
if (r < 0)
return r;
assert(iovec_is_valid(input));
assert(ret);
+ /* Only one of these two flags may be set at the same time */
+ assert(!FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL) || !FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL));
+
if (!sd_id128_in_set(with_key,
_CRED_AUTO,
_CRED_AUTO_INITRD,
} else
id = with_key;
- if (sd_id128_equal(id, CRED_AES256_GCM_BY_NULL) && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL))
- log_warning("Using a null key for encryption and signing. Confidentiality or authenticity will not be provided.");
+ if (sd_id128_equal(id, CRED_AES256_GCM_BY_NULL)) {
+ if (FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL))
+ return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Attempted to encrypt with null key, but this is disallowed.");
+ if (!FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL))
+ 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, &tpm2_key, md);
assert(iovec_is_valid(input));
assert(ret);
+ /* Only one of these two flags may be set at the same time */
+ assert(!FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL) || !FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL));
+
/* Relevant error codes:
*
* -EBADMSG → Corrupted file
* -EOPNOTSUPP → Unsupported file type (could be: requires TPM but we have no TPM)
* -EHOSTDOWN → Need PCR signature file, but couldn't find it
- * -EHWPOISON → Attempt to decode NULL key (and CREDENTIAL_ALLOW_NULL is off), but the system has a TPM and SecureBoot is on
+ * -EHWPOISON → Attempt to unlock with NULL key and either CREDENTIAL_ALLOW_REFUSE is on, or CREDENTIAL_ALLOW_NULL is off, but the system has a TPM and SecureBoot is on
* -EMEDIUMTYPE → File has unexpected scope, i.e. user-scoped credential is attempted to be unlocked in system scope, or vice versa
* -EDESTADDRREQ → Credential is incorrectly named (i.e. the authenticated name does not match the actual name)
* -ESTALE → Credential's validity has passed
return log_error_errno(r, "Failed to load PCR signature: %m");
}
- if (with_null && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) {
- /* 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(EHWPOISON),
- "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.");
+ if (with_null) {
+ if (FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL))
+ return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON),
+ "Credential uses null key, but that's not allowed, refusing.");
+
+ if (!FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) {
+ /* 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(EHWPOISON),
+ "Credential uses null key intended for fallback use when TPM2 is absent — but TPM2 is present, and SecureBoot is enabled, refusing.");
+
+ log_warning("Credential uses null key intended for use when TPM2 is absent, but TPM2 is present! Accepting anyway, since SecureBoot is disabled.");
+ } else
+ log_debug("Credential uses null key intended for use when TPM2 is absent, and TPM2 indeed is absent. Accepting.");
+ }
}
if (with_scope) {
SD_JSON_BUILD_PAIR_CONDITION(not_after != USEC_INFINITY, "notAfter", SD_JSON_BUILD_UNSIGNED(not_after)),
SD_JSON_BUILD_PAIR_CONDITION(!FLAGS_SET(flags, CREDENTIAL_ANY_SCOPE), "scope", SD_JSON_BUILD_STRING(uid_is_valid(uid) ? "user" : "system")),
SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", SD_JSON_BUILD_UNSIGNED(uid)),
+ SD_JSON_BUILD_PAIR_CONDITION((flags & (CREDENTIAL_ALLOW_NULL|CREDENTIAL_REFUSE_NULL)) != 0, "allowNull", SD_JSON_BUILD_BOOLEAN(flags & CREDENTIAL_ALLOW_NULL)),
SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", FLAGS_SET(flags, CREDENTIAL_IPC_ALLOW_INTERACTIVE)));
if (r < 0)
return log_error_errno(r, "Failed to call Encrypt() varlink call.");
int get_credential_user_password(const char *username, char **ret_password, bool *ret_is_hashed);
typedef enum CredentialFlags {
- CREDENTIAL_ALLOW_NULL = 1 << 0, /* allow decryption of NULL key, even if TPM is around */
- CREDENTIAL_ANY_SCOPE = 1 << 1, /* allow decryption of both system and user credentials */
+ CREDENTIAL_ALLOW_NULL = 1 << 0, /* allow decryption with NULL key, even if TPM is around */
+ CREDENTIAL_REFUSE_NULL = 1 << 1, /* deny decryption with NULL key, even if SecureBoot is off */
+ CREDENTIAL_ANY_SCOPE = 1 << 2, /* allow decryption of both system and user credentials */
/* Only used by ipc_{encrypt,decrypt}_credential */
- CREDENTIAL_IPC_ALLOW_INTERACTIVE = 1 << 2,
+ CREDENTIAL_IPC_ALLOW_INTERACTIVE = 1 << 3,
} CredentialFlags;
/* The four modes we support: keyed only by on-disk key, only by TPM2 HMAC key, and by the combination of
SD_VARLINK_DEFINE_INPUT_BY_TYPE(scope, Scope, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("If the 'user' scope is selected, specifies the numeric UNIX UID of the user the credential is associated with. If not specified this is automatically derived from the UID of the calling user, if that can be determined."),
SD_VARLINK_DEFINE_INPUT(uid, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("If true allows decryption of credentials encrypted with the null key, if false does not allow it, if unset/null the default depends on whether a TPM device exists and SecureBoot is enabled."),
+ SD_VARLINK_DEFINE_INPUT(allowNull, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
VARLINK_DEFINE_POLKIT_INPUT,
SD_VARLINK_FIELD_COMMENT("The decrypted plaintext data in Base64 encoding."),
SD_VARLINK_DEFINE_OUTPUT(data, SD_VARLINK_STRING, 0));
rm /tmp/vlcredsdata2
varlinkctl call /run/systemd/io.systemd.Credentials io.systemd.Credentials.Encrypt "{\"data\":\"$DATA\",\"withKey\":\"null\"}" | \
+ jq '.["allowNull"] = true' |
varlinkctl call --json=short /run/systemd/io.systemd.Credentials io.systemd.Credentials.Decrypt > /tmp/vlcredsdata2
cmp /tmp/vlcredsdata /tmp/vlcredsdata2
rm /tmp/vlcredsdata /tmp/vlcredsdata2
+# Ensure allowNull works
+(! varlinkctl call /run/systemd/io.systemd.Credentials io.systemd.Credentials.Encrypt "{\"data\":\"$DATA\",\"withKey\":\"null\"}" | \
+ jq '.["allowNull"] = false' |
+ varlinkctl call --json=short /run/systemd/io.systemd.Credentials io.systemd.Credentials.Decrypt )
+
clean_usertest() {
rm -f /tmp/usertest.data /tmp/usertest.data /tmp/brummbaer.data
}