| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <errno.h> |
| #include <libcryptsetup.h> |
| |
| #include "cryptsetup-token.h" |
| #include "cryptsetup-token-util.h" |
| #include "hexdecoct.h" |
| #include "json.h" |
| #include "luks2-tpm2.h" |
| #include "memory-util.h" |
| #include "strv.h" |
| #include "tpm2-util.h" |
| #include "version.h" |
| |
| #define TOKEN_NAME "systemd-tpm2" |
| #define TOKEN_VERSION_MAJOR "1" |
| #define TOKEN_VERSION_MINOR "0" |
| |
| /* for libcryptsetup debug purpose */ |
| _public_ const char *cryptsetup_token_version(void) { |
| |
| return TOKEN_VERSION_MAJOR "." TOKEN_VERSION_MINOR " systemd-v" STRINGIFY(PROJECT_VERSION) " (" GIT_VERSION ")"; |
| } |
| |
| static int log_debug_open_error(struct crypt_device *cd, int r) { |
| if (r == -EAGAIN) |
| return crypt_log_debug_errno(cd, r, "TPM2 device not found."); |
| if (r == -ENXIO) |
| return crypt_log_debug_errno(cd, r, "No matching TPM2 token data found."); |
| |
| return crypt_log_debug_errno(cd, r, TOKEN_NAME " open failed: %m."); |
| } |
| |
| _public_ int cryptsetup_token_open_pin( |
| struct crypt_device *cd, /* is always LUKS2 context */ |
| int token /* is always >= 0 */, |
| const char *pin, |
| size_t pin_size, |
| char **ret_password, /* freed by cryptsetup_token_buffer_free */ |
| size_t *ret_password_len, |
| void *usrptr /* plugin defined parameter passed to crypt_activate_by_token*() API */) { |
| |
| _cleanup_(erase_and_freep) char *base64_encoded = NULL, *pin_string = NULL; |
| _cleanup_free_ void *blob = NULL, *pubkey = NULL, *policy_hash = NULL, *salt = NULL; |
| size_t blob_size, policy_hash_size, decrypted_key_size, pubkey_size, salt_size = 0; |
| _cleanup_(erase_and_freep) void *decrypted_key = NULL; |
| _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; |
| uint32_t hash_pcr_mask, pubkey_pcr_mask; |
| systemd_tpm2_plugin_params params = { |
| .search_pcr_mask = UINT32_MAX |
| }; |
| uint16_t pcr_bank, primary_alg; |
| ssize_t base64_encoded_size; |
| TPM2Flags flags = 0; |
| const char *json; |
| int r; |
| |
| assert(token >= 0); |
| assert(!pin || pin_size > 0); |
| assert(ret_password); |
| assert(ret_password_len); |
| |
| /* This must not fail at this moment (internal error) */ |
| r = crypt_token_json_get(cd, token, &json); |
| assert(token == r); |
| assert(json); |
| |
| r = crypt_normalize_pin(pin, pin_size, &pin_string); |
| if (r < 0) |
| return crypt_log_debug_errno(cd, r, "Can not normalize PIN: %m"); |
| |
| if (usrptr) |
| params = *(systemd_tpm2_plugin_params *)usrptr; |
| |
| r = json_parse(json, 0, &v, NULL, NULL); |
| if (r < 0) |
| return crypt_log_debug_errno(cd, r, "Failed to parse token JSON data: %m"); |
| |
| r = tpm2_parse_luks2_json( |
| v, |
| NULL, |
| &hash_pcr_mask, |
| &pcr_bank, |
| &pubkey, |
| &pubkey_size, |
| &pubkey_pcr_mask, |
| &primary_alg, |
| &blob, |
| &blob_size, |
| &policy_hash, |
| &policy_hash_size, |
| &salt, |
| &salt_size, |
| &flags); |
| if (r < 0) |
| return log_debug_open_error(cd, r); |
| |
| if (params.search_pcr_mask != UINT32_MAX && hash_pcr_mask != params.search_pcr_mask) |
| return crypt_log_debug_errno(cd, ENXIO, "PCR mask doesn't match expectation (%" PRIu32 " vs. %" PRIu32 ")", hash_pcr_mask, params.search_pcr_mask); |
| |
| r = acquire_luks2_key( |
| params.device, |
| hash_pcr_mask, |
| pcr_bank, |
| pubkey, pubkey_size, |
| pubkey_pcr_mask, |
| params.signature_path, |
| pin_string, |
| primary_alg, |
| blob, |
| blob_size, |
| policy_hash, |
| policy_hash_size, |
| salt, |
| salt_size, |
| flags, |
| &decrypted_key, |
| &decrypted_key_size); |
| if (r < 0) |
| return log_debug_open_error(cd, r); |
| |
| /* Before using this key as passphrase we base64 encode it, for compat with homed */ |
| base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); |
| if (base64_encoded_size < 0) |
| return log_debug_open_error(cd, base64_encoded_size); |
| |
| /* free'd automatically by libcryptsetup */ |
| *ret_password = TAKE_PTR(base64_encoded); |
| *ret_password_len = base64_encoded_size; |
| |
| return 0; |
| } |
| |
| /* |
| * This function is called from within following libcryptsetup calls |
| * provided conditions further below are met: |
| * |
| * crypt_activate_by_token(), crypt_activate_by_token_type(type == 'systemd-tpm2'): |
| * |
| * - token is assigned to at least one luks2 keyslot eligible to activate LUKS2 device |
| * (alternatively: name is set to null, flags contains CRYPT_ACTIVATE_ALLOW_UNBOUND_KEY |
| * and token is assigned to at least single keyslot). |
| * |
| * - if plugin defines validate function (see cryptsetup_token_validate below) it must have |
| * passed the check (aka return 0) |
| */ |
| _public_ int cryptsetup_token_open( |
| struct crypt_device *cd, /* is always LUKS2 context */ |
| int token /* is always >= 0 */, |
| char **ret_password, /* freed by cryptsetup_token_buffer_free */ |
| size_t *ret_password_len, |
| void *usrptr /* plugin defined parameter passed to crypt_activate_by_token*() API */) { |
| |
| return cryptsetup_token_open_pin(cd, token, NULL, 0, ret_password, ret_password_len, usrptr); |
| } |
| |
| /* |
| * libcryptsetup callback for memory deallocation of 'password' parameter passed in |
| * any crypt_token_open_* plugin function |
| */ |
| _public_ void cryptsetup_token_buffer_free(void *buffer, size_t buffer_len) { |
| erase_and_free(buffer); |
| } |
| |
| /* |
| * prints systemd-tpm2 token content in crypt_dump(). |
| * 'type' and 'keyslots' fields are printed by libcryptsetup |
| */ |
| _public_ void cryptsetup_token_dump( |
| struct crypt_device *cd /* is always LUKS2 context */, |
| const char *json /* validated 'systemd-tpm2' token if cryptsetup_token_validate is defined */) { |
| |
| _cleanup_free_ char *hash_pcrs_str = NULL, *pubkey_pcrs_str = NULL, *blob_str = NULL, *policy_hash_str = NULL, *pubkey_str = NULL; |
| _cleanup_free_ void *blob = NULL, *pubkey = NULL, *policy_hash = NULL, *salt = NULL; |
| _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; |
| size_t blob_size, policy_hash_size, pubkey_size, salt_size = 0; |
| uint32_t hash_pcr_mask, pubkey_pcr_mask; |
| uint16_t pcr_bank, primary_alg; |
| TPM2Flags flags = 0; |
| int r; |
| |
| assert(json); |
| |
| r = json_parse(json, 0, &v, NULL, NULL); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Failed to parse " TOKEN_NAME " JSON object: %m"); |
| |
| r = tpm2_parse_luks2_json( |
| v, |
| NULL, |
| &hash_pcr_mask, |
| &pcr_bank, |
| &pubkey, |
| &pubkey_size, |
| &pubkey_pcr_mask, |
| &primary_alg, |
| &blob, |
| &blob_size, |
| &policy_hash, |
| &policy_hash_size, |
| &salt, |
| &salt_size, |
| &flags); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Failed to parse " TOKEN_NAME " JSON fields: %m"); |
| |
| r = pcr_mask_to_string(hash_pcr_mask, &hash_pcrs_str); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Cannot format PCR hash mask: %m"); |
| |
| r = pcr_mask_to_string(pubkey_pcr_mask, &pubkey_pcrs_str); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Cannot format PCR hash mask: %m"); |
| |
| r = crypt_dump_buffer_to_hex_string(blob, blob_size, &blob_str); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Can not dump " TOKEN_NAME " content: %m"); |
| |
| r = crypt_dump_buffer_to_hex_string(pubkey, pubkey_size, &pubkey_str); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Can not dump " TOKEN_NAME " content: %m"); |
| |
| r = crypt_dump_buffer_to_hex_string(policy_hash, policy_hash_size, &policy_hash_str); |
| if (r < 0) |
| return (void) crypt_log_debug_errno(cd, r, "Can not dump " TOKEN_NAME " content: %m"); |
| |
| crypt_log(cd, "\ttpm2-hash-pcrs: %s\n", strna(hash_pcrs_str)); |
| crypt_log(cd, "\ttpm2-pcr-bank: %s\n", strna(tpm2_hash_alg_to_string(pcr_bank))); |
| crypt_log(cd, "\ttpm2-pubkey:" CRYPT_DUMP_LINE_SEP "%s\n", pubkey_str); |
| crypt_log(cd, "\ttpm2-pubkey-pcrs: %s\n", strna(pubkey_pcrs_str)); |
| crypt_log(cd, "\ttpm2-primary-alg: %s\n", strna(tpm2_asym_alg_to_string(primary_alg))); |
| crypt_log(cd, "\ttpm2-blob: %s\n", blob_str); |
| crypt_log(cd, "\ttpm2-policy-hash:" CRYPT_DUMP_LINE_SEP "%s\n", policy_hash_str); |
| crypt_log(cd, "\ttpm2-pin: %s\n", true_false(flags & TPM2_FLAGS_USE_PIN)); |
| crypt_log(cd, "\ttpm2-salt: %s\n", true_false(salt)); |
| } |
| |
| /* |
| * Note: |
| * If plugin is available in library path, it's called in before following libcryptsetup calls: |
| * |
| * crypt_token_json_set, crypt_dump, any crypt_activate_by_token_* flavour |
| */ |
| _public_ int cryptsetup_token_validate( |
| struct crypt_device *cd, /* is always LUKS2 context */ |
| const char *json /* contains valid 'type' and 'keyslots' fields. 'type' is 'systemd-tpm2' */) { |
| |
| int r; |
| JsonVariant *w, *e; |
| _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; |
| |
| assert(json); |
| |
| r = json_parse(json, 0, &v, NULL, NULL); |
| if (r < 0) |
| return crypt_log_debug_errno(cd, r, "Could not parse " TOKEN_NAME " json object: %m"); |
| |
| w = json_variant_by_key(v, "tpm2-pcrs"); |
| if (!w || !json_variant_is_array(w)) { |
| crypt_log_debug(cd, "TPM2 token data lacks 'tpm2-pcrs' field."); |
| return 1; |
| } |
| |
| JSON_VARIANT_ARRAY_FOREACH(e, w) { |
| uint64_t u; |
| |
| if (!json_variant_is_number(e)) { |
| crypt_log_debug(cd, "TPM2 PCR is not a number."); |
| return 1; |
| } |
| |
| u = json_variant_unsigned(e); |
| if (u >= TPM2_PCRS_MAX) { |
| crypt_log_debug(cd, "TPM2 PCR number out of range."); |
| return 1; |
| } |
| } |
| |
| /* The bank field is optional, since it was added in systemd 250 only. Before the bank was hardcoded |
| * to SHA256. */ |
| w = json_variant_by_key(v, "tpm2-pcr-bank"); |
| if (w) { |
| /* The PCR bank field is optional */ |
| |
| if (!json_variant_is_string(w)) { |
| crypt_log_debug(cd, "TPM2 PCR bank is not a string."); |
| return 1; |
| } |
| |
| if (tpm2_hash_alg_from_string(json_variant_string(w)) < 0) { |
| crypt_log_debug(cd, "TPM2 PCR bank invalid or not supported: %s.", json_variant_string(w)); |
| return 1; |
| } |
| } |
| |
| /* The primary key algorithm field is optional, since it was also added in systemd 250 only. Before |
| * the algorithm was hardcoded to ECC. */ |
| w = json_variant_by_key(v, "tpm2-primary-alg"); |
| if (w) { |
| /* The primary key algorithm is optional */ |
| |
| if (!json_variant_is_string(w)) { |
| crypt_log_debug(cd, "TPM2 primary key algorithm is not a string."); |
| return 1; |
| } |
| |
| if (tpm2_asym_alg_from_string(json_variant_string(w)) < 0) { |
| crypt_log_debug(cd, "TPM2 primary key algorithm invalid or not supported: %s", json_variant_string(w)); |
| return 1; |
| } |
| } |
| |
| w = json_variant_by_key(v, "tpm2-blob"); |
| if (!w || !json_variant_is_string(w)) { |
| crypt_log_debug(cd, "TPM2 token data lacks 'tpm2-blob' field."); |
| return 1; |
| } |
| |
| r = unbase64mem(json_variant_string(w), SIZE_MAX, NULL, NULL); |
| if (r < 0) |
| return crypt_log_debug_errno(cd, r, "Invalid base64 data in 'tpm2-blob' field: %m"); |
| |
| w = json_variant_by_key(v, "tpm2-policy-hash"); |
| if (!w || !json_variant_is_string(w)) { |
| crypt_log_debug(cd, "TPM2 token data lacks 'tpm2-policy-hash' field."); |
| return 1; |
| } |
| |
| r = unhexmem(json_variant_string(w), SIZE_MAX, NULL, NULL); |
| if (r < 0) |
| return crypt_log_debug_errno(cd, r, "Invalid base64 data in 'tpm2-policy-hash' field: %m"); |
| |
| w = json_variant_by_key(v, "tpm2-pin"); |
| if (w) { |
| if (!json_variant_is_boolean(w)) { |
| crypt_log_debug(cd, "TPM2 PIN policy is not a boolean."); |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |