#include "fs-util.h"
#include "hexdecoct.h"
#include "memory-util.h"
+#include "openssl-util.h"
#include "random-util.h"
#include "sha256.h"
#include "time-util.h"
return 0;
}
-static int tpm2_make_pcr_session(
+static int openssl_pubkey_to_tpm2_pubkey(EVP_PKEY *input, TPM2B_PUBLIC *output) {
+#if OPENSSL_VERSION_MAJOR >= 3
+ _cleanup_(BN_freep) BIGNUM *n = NULL, *e = NULL;
+#else
+ const BIGNUM *n = NULL, *e = NULL;
+ const RSA *rsa = NULL;
+#endif
+ int n_bytes, e_bytes;
+
+ assert(input);
+ assert(output);
+
+ /* Converts an OpenSSL public key to a structure that the TPM chip can process. */
+
+ if (EVP_PKEY_base_id(input) != EVP_PKEY_RSA)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided public key is not an RSA key.");
+
+#if OPENSSL_VERSION_MAJOR >= 3
+ if (!EVP_PKEY_get_bn_param(input, OSSL_PKEY_PARAM_RSA_N, &n))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get RSA modulus from public key.");
+#else
+ rsa = EVP_PKEY_get0_RSA(input);
+ if (!rsa)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to extract RSA key from public key.");
+
+ n = RSA_get0_n(rsa);
+ if (!n)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get RSA modulus from public key.");
+#endif
+
+ n_bytes = BN_num_bytes(n);
+ assert_se(n_bytes > 0);
+ if ((size_t) n_bytes > sizeof_field(TPM2B_PUBLIC, publicArea.unique.rsa.buffer))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "RSA modulus too large for TPM2 public key object.");
+
+#if OPENSSL_VERSION_MAJOR >= 3
+ if (!EVP_PKEY_get_bn_param(input, OSSL_PKEY_PARAM_RSA_E, &e))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get RSA exponent from public key.");
+#else
+ e = RSA_get0_e(rsa);
+ if (!e)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get RSA exponent from public key.");
+#endif
+
+ e_bytes = BN_num_bytes(e);
+ assert_se(e_bytes > 0);
+ if ((size_t) e_bytes > sizeof_field(TPM2B_PUBLIC, publicArea.parameters.rsaDetail.exponent))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "RSA exponent too large for TPM2 public key object.");
+
+ *output = (TPM2B_PUBLIC) {
+ .size = sizeof(TPMT_PUBLIC),
+ .publicArea = {
+ .type = TPM2_ALG_RSA,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes = TPMA_OBJECT_DECRYPT | TPMA_OBJECT_SIGN_ENCRYPT | TPMA_OBJECT_USERWITHAUTH,
+ .parameters.rsaDetail = {
+ .scheme = {
+ .scheme = TPM2_ALG_NULL,
+ .details.anySig.hashAlg = TPM2_ALG_NULL,
+ },
+ .symmetric = {
+ .algorithm = TPM2_ALG_NULL,
+ .mode.sym = TPM2_ALG_NULL,
+ },
+ .keyBits = n_bytes * 8,
+ /* .exponent will be filled in below. */
+ },
+ .unique = {
+ .rsa.size = n_bytes,
+ /* .rsa.buffer will be filled in below. */
+ },
+ },
+ };
+
+ if (BN_bn2bin(n, output->publicArea.unique.rsa.buffer) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to convert RSA modulus.");
+
+ if (BN_bn2bin(e, (unsigned char*) &output->publicArea.parameters.rsaDetail.exponent) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to convert RSA exponent.");
+
+ return 0;
+}
+
+static int find_signature(
+ JsonVariant *v,
+ uint16_t pcr_bank,
+ uint32_t pcr_mask,
+ EVP_PKEY *pk,
+ const void *policy,
+ size_t policy_size,
+ void *ret_signature,
+ size_t *ret_signature_size) {
+
+ _cleanup_free_ void *fp = NULL;
+ JsonVariant *b, *i;
+ size_t fp_size;
+ const char *k;
+ int r;
+
+ /* Searches for a signature blob in the specified JSON object. Search keys are PCR bank, PCR mask,
+ * public key, and policy digest. */
+
+ if (!json_variant_is_object(v))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Signature is not a JSON object.");
+
+ k = tpm2_pcr_bank_to_string(pcr_bank);
+ if (!k)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Don't know PCR bank %" PRIu16, pcr_bank);
+
+ /* First, find field by bank */
+ b = json_variant_by_key(v, k);
+ if (!b)
+ return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Signature lacks data for PCR bank '%s'.", k);
+
+ if (!json_variant_is_array(b))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Bank data is not a JSON array.");
+
+ /* Now iterate through all signatures known for this bank */
+ JSON_VARIANT_ARRAY_FOREACH(i, b) {
+ _cleanup_free_ void *fpj_data = NULL, *polj_data = NULL;
+ JsonVariant *maskj, *fpj, *sigj, *polj;
+ size_t fpj_size, polj_size;
+ uint32_t parsed_mask;
+
+ if (!json_variant_is_object(i))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Bank data element is not a JSON object");
+
+ /* Check if the PCR mask matches our expectations */
+ maskj = json_variant_by_key(i, "pcrs");
+ if (!maskj)
+ continue;
+
+ r = tpm2_parse_pcr_json_array(maskj, &parsed_mask);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON PCR mask");
+
+ if (parsed_mask != pcr_mask)
+ continue; /* Not for this PCR mask */
+
+ /* Then check if this is for the public key we operate with */
+ fpj = json_variant_by_key(i, "pkfp");
+ if (!fpj)
+ continue;
+
+ r = json_variant_unhex(fpj, &fpj_data, &fpj_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode fingerprint in JSON data: %m");
+
+ if (!fp) {
+ r = pubkey_fingerprint(pk, EVP_sha256(), &fp, &fp_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to calculate public key fingerprint: %m");
+ }
+
+ if (memcmp_nn(fp, fp_size, fpj_data, fpj_size) != 0)
+ continue; /* Not for this public key */
+
+ /* Finally, check if this is for the PCR policy we expect this to be */
+ polj = json_variant_by_key(i, "pol");
+ if (!polj)
+ continue;
+
+ r = json_variant_unhex(polj, &polj_data, &polj_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode policy hash JSON data: %m");
+
+ if (memcmp_nn(policy, policy_size, polj_data, polj_size) != 0)
+ continue;
+
+ /* This entry matches all our expectations, now return the signature included in it */
+ sigj = json_variant_by_key(i, "sig");
+ if (!sigj)
+ continue;
+
+ return json_variant_unbase64(sigj, ret_signature, ret_signature_size);
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Couldn't find signature for this PCR bank, PCR index and public key.");
+}
+
+static int tpm2_make_policy_session(
ESYS_CONTEXT *c,
ESYS_TR primary,
ESYS_TR parent_session,
TPM2_SE session_type,
- uint32_t pcr_mask,
+ uint32_t hash_pcr_mask,
uint16_t pcr_bank, /* If UINT16_MAX, pick best bank automatically, otherwise specify bank explicitly. */
+ const void *pubkey,
+ size_t pubkey_size,
+ uint32_t pubkey_pcr_mask,
+ JsonVariant *signature_json,
bool use_pin,
ESYS_TR *ret_session,
TPM2B_DIGEST **ret_policy_digest,
.mode.aes = TPM2_ALG_CFB,
};
_cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL;
- TPML_PCR_SELECTION pcr_selection;
- ESYS_TR session = ESYS_TR_NONE;
+ ESYS_TR session = ESYS_TR_NONE, pubkey_handle = ESYS_TR_NONE;
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL;
TSS2_RC rc;
int r;
assert(c);
+ assert(pubkey || pubkey_size == 0);
+ assert(pubkey_pcr_mask == 0 || pubkey_size > 0);
log_debug("Starting authentication session.");
- if (pcr_bank != UINT16_MAX) {
- r = tpm2_pcr_mask_good(c, pcr_bank, pcr_mask);
- if (r < 0)
- return r;
- if (r == 0)
- log_notice("Selected TPM2 PCRs are not initialized on this system, most likely due to a firmware issue. PCR policy is effectively not enforced. Proceeding anyway.");
+ if ((hash_pcr_mask | pubkey_pcr_mask) != 0) {
+ /* We are told to configure a PCR policy of some form, let's determine/validate the PCR bank to use. */
- tpm2_pcr_mask_to_selection(pcr_mask, pcr_bank, &pcr_selection);
- } else {
- TPMI_ALG_HASH h;
+ if (pcr_bank != UINT16_MAX) {
+ r = tpm2_pcr_mask_good(c, pcr_bank, hash_pcr_mask|pubkey_pcr_mask);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ log_warning("Selected TPM2 PCRs are not initialized on this system, most likely due to a firmware issue. PCR policy is effectively not enforced. Proceeding anyway.");
+ } else {
+ /* No bank configured, pick automatically. Some TPM2 devices only can do SHA1. If we
+ * detect that use that, but preferably use SHA256 */
+ r = tpm2_get_best_pcr_bank(c, hash_pcr_mask|pubkey_pcr_mask, &pcr_bank);
+ if (r < 0)
+ return r;
+ }
+ }
- /* No bank configured, pick automatically. Some TPM2 devices only can do SHA1. If we detect
- * that use that, but preferably use SHA256 */
- r = tpm2_get_best_pcr_bank(c, pcr_mask, &h);
- if (r < 0)
- return r;
+ if (pubkey_size > 0) {
+ /* If a pubkey is specified, load it to validate it, even if the PCR mask for this is actually zero, and we are thus not going to use it. */
+ _cleanup_fclose_ FILE *f = fmemopen((void*) pubkey, pubkey_size, "r");
+ if (!f)
+ return log_oom();
- tpm2_pcr_mask_to_selection(pcr_mask, h, &pcr_selection);
+ pk = PEM_read_PUBKEY(f, NULL, NULL, NULL);
+ if (!pk)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse PEM public key.");
}
rc = sym_Esys_StartAuthSession(
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
"Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc));
- log_debug("Configuring PCR policy.");
+ if (pubkey_pcr_mask != 0) {
+ log_debug("Configuring public key based PCR policy.");
- rc = sym_Esys_PolicyPCR(
- c,
- session,
- ESYS_TR_NONE,
- ESYS_TR_NONE,
- ESYS_TR_NONE,
- NULL,
- &pcr_selection);
- if (rc != TSS2_RC_SUCCESS) {
- r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
- "Failed to add PCR policy to TPM: %s", sym_Tss2_RC_Decode(rc));
- goto finish;
+ /* First: load public key into the TPM */
+ TPM2B_PUBLIC pubkey_tpm2;
+ r = openssl_pubkey_to_tpm2_pubkey(pk, &pubkey_tpm2);
+ if (r < 0)
+ goto finish;
+
+ rc = sym_Esys_LoadExternal(
+ c,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ &pubkey_tpm2,
+ TPM2_RH_OWNER,
+ &pubkey_handle);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to load public key into TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ /* Acquire the "name" of what we just loaded */
+ _cleanup_(Esys_Freep) TPM2B_NAME *pubkey_name = NULL;
+ rc = sym_Esys_TR_GetName(
+ c,
+ pubkey_handle,
+ &pubkey_name);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to get name of public key from TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ /* Put together the PCR policy we want to use */
+ TPML_PCR_SELECTION pcr_selection;
+ tpm2_pcr_mask_to_selection(pubkey_pcr_mask, pcr_bank, &pcr_selection);
+ rc = sym_Esys_PolicyPCR(
+ c,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ &pcr_selection);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to add PCR policy to TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ /* Get the policy hash of the PCR policy */
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *approved_policy = NULL;
+ rc = sym_Esys_PolicyGetDigest(
+ c,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &approved_policy);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ /* When we are unlocking and have a signature, let's pass it to the TPM */
+ _cleanup_(Esys_Freep) TPMT_TK_VERIFIED *check_ticket_buffer = NULL;
+ const TPMT_TK_VERIFIED *check_ticket;
+ if (signature_json) {
+ _cleanup_free_ void *signature_raw = NULL;
+ size_t signature_size;
+
+ r = find_signature(
+ signature_json,
+ pcr_bank,
+ pubkey_pcr_mask,
+ pk,
+ approved_policy->buffer,
+ approved_policy->size,
+ &signature_raw,
+ &signature_size);
+ if (r < 0)
+ goto finish;
+
+ /* TPM2_VerifySignature() will only verify the RSA part of the RSA+SHA256 signature,
+ * hence we need to do the SHA256 part outselves, first */
+ TPM2B_DIGEST signature_hash = {
+ .size = SHA256_DIGEST_SIZE,
+ };
+ assert(sizeof(signature_hash.buffer) >= SHA256_DIGEST_SIZE);
+ sha256_direct(approved_policy->buffer, approved_policy->size, signature_hash.buffer);
+
+ TPMT_SIGNATURE policy_signature = {
+ .sigAlg = TPM2_ALG_RSASSA,
+ .signature.rsassa = {
+ .hash = TPM2_ALG_SHA256,
+ .sig.size = signature_size,
+ },
+ };
+ if (signature_size > sizeof(policy_signature.signature.rsassa.sig.buffer)) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Signature larger than buffer.");
+ goto finish;
+ }
+ memcpy(policy_signature.signature.rsassa.sig.buffer, signature_raw, signature_size);
+
+ rc = sym_Esys_VerifySignature(
+ c,
+ pubkey_handle,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &signature_hash,
+ &policy_signature,
+ &check_ticket_buffer);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to validate signature in TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ check_ticket = check_ticket_buffer;
+ } else {
+ /* When enrolling, we pass a NULL ticket */
+ static const TPMT_TK_VERIFIED check_ticket_null = {
+ .tag = TPM2_ST_VERIFIED,
+ .hierarchy = TPM2_RH_OWNER,
+ };
+
+ check_ticket = &check_ticket_null;
+ }
+
+ rc = sym_Esys_PolicyAuthorize(
+ c,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ approved_policy,
+ /* policyRef= */ &(const TPM2B_NONCE) {},
+ pubkey_name,
+ check_ticket);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to push Authorize policy into TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+ }
+
+ if (hash_pcr_mask != 0) {
+ log_debug("Configuring hash-based PCR policy.");
+
+ TPML_PCR_SELECTION pcr_selection;
+ tpm2_pcr_mask_to_selection(hash_pcr_mask, pcr_bank, &pcr_selection);
+ rc = sym_Esys_PolicyPCR(
+ c,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ &pcr_selection);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to add PCR policy to TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
}
if (use_pin) {
+ log_debug("Configuring PIN policy.");
+
rc = sym_Esys_PolicyAuthValue(
c,
session,
*ret_policy_digest = TAKE_PTR(policy_digest);
if (ret_pcr_bank)
- *ret_pcr_bank = pcr_selection.pcrSelections[0].hash;
+ *ret_pcr_bank = pcr_bank;
r = 0;
finish:
session = tpm2_flush_context_verbose(c, session);
+ pubkey_handle = tpm2_flush_context_verbose(c, pubkey_handle);
return r;
}
-int tpm2_seal(
- const char *device,
- uint32_t pcr_mask,
- const char *pin,
- void **ret_secret,
- size_t *ret_secret_size,
- void **ret_blob,
- size_t *ret_blob_size,
- void **ret_pcr_hash,
- size_t *ret_pcr_hash_size,
- uint16_t *ret_pcr_bank,
- uint16_t *ret_primary_alg) {
+int tpm2_seal(const char *device,
+ uint32_t hash_pcr_mask,
+ const void *pubkey,
+ const size_t pubkey_size,
+ uint32_t pubkey_pcr_mask,
+ const char *pin,
+ void **ret_secret,
+ size_t *ret_secret_size,
+ void **ret_blob,
+ size_t *ret_blob_size,
+ void **ret_pcr_hash,
+ size_t *ret_pcr_hash_size,
+ uint16_t *ret_pcr_bank,
+ uint16_t *ret_primary_alg) {
_cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
_cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL;
TSS2_RC rc;
int r;
+ assert(pubkey || pubkey_size == 0);
+
assert(ret_secret);
assert(ret_secret_size);
assert(ret_blob);
assert(ret_pcr_hash_size);
assert(ret_pcr_bank);
- assert(TPM2_PCR_MASK_VALID(pcr_mask));
+ assert(TPM2_PCR_MASK_VALID(hash_pcr_mask));
+ assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask));
/* So here's what we do here: we connect to the TPM2 chip. It persistently contains a "seed" key that
* is randomized when the TPM2 is first initialized or reset and remains stable across boots. We
if (r < 0)
goto finish;
- r = tpm2_make_pcr_session(
+ r = tpm2_make_policy_session(
c.esys_context,
primary,
session,
TPM2_SE_TRIAL,
- pcr_mask,
+ hash_pcr_mask,
/* pcr_bank= */ UINT16_MAX,
+ pubkey, pubkey_size,
+ pubkey_pcr_mask,
+ /* signature_json= */ NULL,
!!pin,
/* ret_session= */ NULL,
&policy_digest,
return r;
}
-int tpm2_unseal(
- const char *device,
- uint32_t pcr_mask,
+int tpm2_unseal(const char *device,
+ uint32_t hash_pcr_mask,
uint16_t pcr_bank,
+ const void *pubkey,
+ size_t pubkey_size,
+ uint32_t pubkey_pcr_mask,
+ JsonVariant *signature,
+ const char *pin,
uint16_t primary_alg,
const void *blob,
size_t blob_size,
const void *known_policy_hash,
size_t known_policy_hash_size,
- const char *pin,
void **ret_secret,
size_t *ret_secret_size) {
assert(blob);
assert(blob_size > 0);
assert(known_policy_hash_size == 0 || known_policy_hash);
+ assert(pubkey_size == 0 || pubkey);
assert(ret_secret);
assert(ret_secret_size);
- assert(TPM2_PCR_MASK_VALID(pcr_mask));
+ assert(TPM2_PCR_MASK_VALID(hash_pcr_mask));
+ assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask));
r = dlopen_tpm2();
if (r < 0)
if (r < 0)
goto finish;
- r = tpm2_make_pcr_session(
+ r = tpm2_make_policy_session(
c.esys_context,
primary,
hmac_session,
TPM2_SE_POLICY,
- pcr_mask,
+ hash_pcr_mask,
pcr_bank,
+ pubkey, pubkey_size,
+ pubkey_pcr_mask,
+ signature,
!!pin,
&session,
&policy_digest,