diff options
| -rw-r--r-- | CMakeLists.txt | 12 | ||||
| -rw-r--r-- | src/keyring.cpp | 318 | ||||
| -rw-r--r-- | src/keyring.h | 27 | ||||
| -rw-r--r-- | src/tpm/tpm.h | 33 | ||||
| -rw-r--r-- | src/tpm/tpm_cng.c | 170 | ||||
| -rw-r--r-- | src/tpm/tpm_stub.c | 28 | ||||
| -rw-r--r-- | src/tpm/tpm_tss.c | 244 |
7 files changed, 757 insertions, 75 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index b3786d9..f9143be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,8 +126,20 @@ qt_add_executable(Attorney_Online src/auth_flow.cpp src/saved_auth.h src/saved_auth.cpp + src/tpm/tpm.h ) +if(WIN32) + target_sources(Attorney_Online PRIVATE src/tpm/tpm_cng.c) + target_link_libraries(Attorney_Online PRIVATE ncrypt rpcrt4) +elseif(UNIX AND NOT APPLE) + target_sources(Attorney_Online PRIVATE src/tpm/tpm_tss.c) + target_link_libraries(Attorney_Online PRIVATE tss2-esys tss2-mu) + target_compile_definitions(Attorney_Online PRIVATE SOF_AO_HARDWARE_KEY_REQUIRES_PIN_UI) +else() + target_sources(Attorney_Online PRIVATE src/tpm/tpm_stub.c) +endif() + if(CMAKE_BUILD_TYPE STREQUAL "Dev") target_compile_definitions(Attorney_Online PRIVATE NETWORK_DEBUG) target_compile_options(Attorney_Online PRIVATE -O0 -Wall -Wextra -pedantic) diff --git a/src/keyring.cpp b/src/keyring.cpp index eb71e53..324aec2 100644 --- a/src/keyring.cpp +++ b/src/keyring.cpp @@ -16,6 +16,10 @@ #include "file_functions.h" #include "keyring.h" +extern "C" { +#include "tpm/tpm.h" +} + // Encrypted secret key serialized format version. // // Version 1: Secret key encrypted with ChaCha20-Poly1305 using password-based @@ -45,6 +49,10 @@ static constexpr quint8 all_zero_nonce[crypto_aead_chacha20poly1305_NPUBBYTES] = // authentication mechanism. static const auto domain_sep = QByteArrayLiteral("Einsof-Auth-DHCR"); +// Internal state, set by tpm2_init if TPM check and self-test succeed, and we +// can use hardware keys. +static int tpm_available = 0; + static QString get_keyring_path() { return QDir(get_app_path()).filePath("keyring.cbor"); @@ -67,10 +75,73 @@ int keyring_initialize() index.close(); } + if (tpm2_init() == STKB_SUCCESS) + { + tpm_available = 1; + } + + return 0; +} + +// Expose a way to query status without making the internal state variable +// global. +// Yes, this is stupid. +int keyring_hardware_available() +{ + return tpm_available; +} + +// Prepare the key CBOR entry and atomically insert it into the keyring. +// To ensure durability, file locking and atomic overwrite are used as +// provided by Qt. Frankly, I don't see the keyring being prone to corruption +// as there's no concurrent access, save for a contrived edge case of a user +// generating/deleting keys simultaneously from two client instances. It's +// done regardless for a good measure. +static int insert_into_keyring(quint8 tag, QStringView name, const QByteArray &pub, const QByteArray &sec) +{ + QCborMap entry; + entry.insert(0, QCborValue(tag)); + entry.insert(1, QCborValue(name)); + entry.insert(2, QCborValue(pub)); + entry.insert(3, QCborValue(sec)); + QFile index_file(get_keyring_path()); + QLockFile index_lock(QDir(get_app_path()).filePath("keyring.lock")); + if (!index_lock.lock()) + { + return 1; + } + if (!index_file.open(QIODevice::ReadOnly)) + { + return 1; + } + QCborStreamReader idx_in(&index_file); + QCborValue index = QCborValue::fromCbor(idx_in); + index_file.close(); + if (!index.isMap()) + { + index = QCborMap(); + } + QCborMap index_map = index.toMap(); + // Key fingerprint is used as its unique identifier. + QByteArray fingerprint; + fingerprint.append(tag); + fingerprint.append(pub); + uchar fpr_hash[crypto_generichash_BYTES]; + crypto_generichash(fpr_hash, sizeof(fpr_hash), (const uchar *)fingerprint.constData(), fingerprint.size(), nullptr, 0); + index_map.insert(QCborValue(QByteArray((const char *)fpr_hash, (qsizetype)sizeof(fpr_hash))), QCborValue(entry)); + QSaveFile index_file_w(get_keyring_path()); + if (!index_file_w.open(QIODevice::WriteOnly)) + { + return 1; + } + index_file_w.write(QCborValue(index_map).toCbor()); + index_file_w.commit(); + index_lock.unlock(); return 0; } -int generate_key(QStringView name, const QByteArray &password) +// Software X25519 keys protected by a passphrase. +int generate_software_key(QStringView name, const QByteArray &password) { // If we let users specify arbitrary parameters, additional constraints must // be enforced: min/max opslimit, type of memlimit (uint32_t vs size_t). @@ -127,94 +198,73 @@ int generate_key(QStringView name, const QByteArray &password) } sodium_munlock(wrap_key, sizeof(wrap_key)); sodium_munlock(secret_key, sizeof(secret_key)); - QByteArray public_key_array((const char *)public_key, (qsizetype)sizeof(public_key)); - quint8 tag = cert_x25519_tag; - - // Key fingerprint is used as its unique identifier. - QByteArray fingerprint; - fingerprint.append(tag); - fingerprint.append(public_key_array); - uchar fpr_hash[crypto_generichash_BYTES]; - crypto_generichash(fpr_hash, sizeof(fpr_hash), (const uchar *)fingerprint.constData(), fingerprint.size(), nullptr, 0); + QByteArray public_key_array((const char *)public_key, (qsizetype)sizeof(public_key)); QByteArray encrypted_secret(packed_header); encrypted_secret.append((const char *)payload, sizeof(payload)); - // Prepare the key CBOR entry and atomically insert it into the keyring. - // To ensure durability, file locking and atomic overwrite are used as - // provided by Qt. Frankly, I don't see the keyring being prone to corruption - // as there's no concurrent access, save for a contrived edge case of a user - // generating/deleting keys simultaneously from two client instances. It's - // done regardless for a good measure. - QCborMap entry; - entry.insert(0, QCborValue(tag)); - entry.insert(1, QCborValue(name)); - entry.insert(2, QCborValue(public_key_array)); - entry.insert(3, QCborValue(encrypted_secret)); - QFile index_file(get_keyring_path()); - QLockFile index_lock(QDir(get_app_path()).filePath("keyring.lock")); - if (!index_lock.lock()) + if (insert_into_keyring(cert_x25519_tag, name, public_key_array, encrypted_secret)) { - return 9; + return 5; } - if (!index_file.open(QIODevice::ReadOnly)) - { - return 6; - } - QCborStreamReader idx_in(&index_file); - QCborValue index = QCborValue::fromCbor(idx_in); - index_file.close(); - if (!index.isMap()) - { - index = QCborMap(); - } - QCborMap index_map = index.toMap(); - index_map.insert(QCborValue(QByteArray((const char *)fpr_hash, (qsizetype)sizeof(fpr_hash))), QCborValue(entry)); - QSaveFile index_file_w(get_keyring_path()); - if (!index_file_w.open(QIODevice::WriteOnly)) - { - return 8; - } - index_file_w.write(QCborValue(index_map).toCbor()); - index_file_w.commit(); - index_lock.unlock(); return 0; } -// Look up the key_id in the keyring, unlock it with supplied password using -// parameters from its header, perform a Diffie-Hellman key exchange with the -// server's ephemeral key to derive a shared secret, and hash it along with -// additional data to prove your authenticity to the server. -ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, QByteArrayView ephemeral_key, QByteArrayView username, AuthResponse &out) +static constexpr quint8 cert_hwkey_p256_tag = 0x33; + +static std::wstring optional_hardware_key_name(QStringView name) { - if (password.size() < crypto_pwhash_PASSWD_MIN || password.size() > crypto_pwhash_PASSWD_MAX) - { - return ResponseResult::invalid_password; - } +#ifdef SOF_AO_HARDWARE_KEY_REQUIRES_PIN_UI + (void)name; + return std::wstring(); +#else + return name.toString().toStdWString(); +#endif +} - QFile index_file(get_keyring_path()); - if (!index_file.open(QIODevice::ReadOnly)) - { - return ResponseResult::inaccessible_keyring; +// Hardware P-256 keys (because most TPMs can only do P-256, unfortunately); +// the TPM verifies the PIN. +int generate_hardware_key(QStringView name, const QByteArray &pin) +{ + // Compressed P-256 public key. + uint8_t pub[33]; + // Protected secret blobs differ by size depending on the platform (and in + // case of TSS2 implementation, size is unspecified). Prepare a sufficient + // buffer to write to, the actual output size will be in `sec_written`. + uint8_t sec[512]; + size_t sec_written = 0; + std::wstring optional_name_wstr = optional_hardware_key_name(name); + stkb_user_input input = { + .pin = (const uint8_t *)pin.constData(), + .pin_len = (size_t)pin.size(), + .key_name = optional_name_wstr.c_str(), + }; + stkb_rc rc = tpm2_keygen(pub, sec, sizeof(sec), &sec_written, input); + switch (rc) { + case STKB_SUCCESS: + break; + default: + return rc; } - QCborStreamReader idx_in(&index_file); - QCborValue index = QCborValue::fromCbor(idx_in); - index_file.close(); - QCborMap tmp_map = index.toMap().value(QCborValue(QByteArray(key_id))).toMap(); - QCborValue val = tmp_map.value(3); - if (!val.isByteArray()) + QByteArray public_array((const char *)pub, (qsizetype)sizeof(pub)); + QByteArray protected_array((const char *)sec, (qsizetype)sec_written); + if (insert_into_keyring(cert_hwkey_p256_tag, name, public_array, protected_array)) + return 1; + return 0; +} + +static ResponseResult compute_with_software_key(quint8 *shared_secret, const QByteArray &encrypted_secret, QByteArrayView password, QByteArrayView ephemeral_key) +{ + if (password.size() < crypto_pwhash_PASSWD_MIN || password.size() > crypto_pwhash_PASSWD_MAX) { - return ResponseResult::corrupted_entry; + return ResponseResult::invalid_password; } - QByteArray encrypted_secret = val.toByteArray(); - val = tmp_map.value(2); - if (!val.isByteArray()) + if (ephemeral_key.size() != crypto_kx_PUBLICKEYBYTES) { - return ResponseResult::corrupted_entry; + return ResponseResult::incompatible_arguments; } - QByteArray public_key = val.toByteArray(); // After retrieving the key from the keyring, the process is the same as // in key generation, but in the opposite direction. @@ -264,7 +314,6 @@ ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, Q // Now we've unlocked the key, and ready to perform DH with the server's // ephemeral key to prove ourselves. - quint8 shared_secret[crypto_scalarmult_BYTES]; if (crypto_scalarmult(shared_secret, secret_key, (const uchar *)ephemeral_key.constData())) { // We ended up with the point at infinity, something stupid must've @@ -274,6 +323,126 @@ ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, Q } sodium_munlock(secret_key, sizeof(secret_key)); + return ResponseResult::success; +} + +static ResponseResult compute_with_hardware_key(quint8 *shared_secret, const QByteArray &blob, QByteArrayView pin, QByteArrayView ephemeral_key) +{ + if (ephemeral_key.size() != 64) + { + return ResponseResult::incompatible_arguments; + } + stkb_user_input input = { + .pin = (const uint8_t *)pin.constData(), + .pin_len = (size_t)pin.size(), + .key_name = nullptr, + }; + stkb_rc rc = tpm2_compute_ss(shared_secret, (const uint8_t *)blob.constData(), (size_t)blob.size(), (const uint8_t *)ephemeral_key.constData(), input); + switch (rc) { + case STKB_SUCCESS: + return ResponseResult::success; + case STKB_ERROR: + return ResponseResult::hardware_fault; + case STKB_BAD_PIN: + case STKB_AUTH_FAILURE: + return ResponseResult::invalid_pin; + case STKB_LOCKOUT: + return ResponseResult::hardware_lockout; + case STKB_TPM_UNAVAILABLE: + default: + return ResponseResult::inaccessible_keyring; + } +} + +KeyringKey acquire_keyring_key(QByteArrayView key_id) +{ + struct KeyringKey key; + QFile index_file(get_keyring_path()); + if (!index_file.open(QIODevice::ReadOnly)) + { + return key; + } + QCborStreamReader idx_in(&index_file); + QCborValue index = QCborValue::fromCbor(idx_in); + index_file.close(); + QCborMap tmp_map = index.toMap().value(QCborValue(QByteArray(key_id))).toMap(); + + QCborValue val = tmp_map.value(2); + if (!val.isByteArray()) + { + return key; + } + key.public_key = val.toByteArray(); + + val = tmp_map.value(3); + if (!val.isByteArray()) + { + return key; + } + key.protected_secret = val.toByteArray(); + + val = tmp_map.value(0); + if (!val.isInteger()) + { + return key; + } + qint64 raw_tag = val.toInteger(); + if (raw_tag < 0 || raw_tag > 255) + { + return key; + } + quint8 tag = (quint8)raw_tag; + switch (tag) { + case cert_x25519_tag: + key.type = KeyringStorageType::software; + break; + case cert_hwkey_p256_tag: + key.type = KeyringStorageType::hardware; + break; + default: + break; + } + + val = tmp_map.value(1); + if (!val.isString()) + { + return key; + } + key.name = val.toString(); + + + key.id = QByteArray(key_id); + + return key; +} + +// Look up the key_id in the keyring, unlock it with supplied password using +// parameters from its header, perform a Diffie-Hellman key exchange with the +// server's ephemeral key to derive a shared secret, and hash it along with +// additional data to prove your authenticity to the server. +ResponseResult unlock_and_auth(const KeyringKey &key, QByteArrayView input, QByteArrayView ephemeral_key, QByteArrayView username, AuthResponse &out) +{ + uint8_t shared_secret[32]; + sodium_mlock(shared_secret, sizeof(shared_secret)); + + ResponseResult result; + switch (key.type) { + case KeyringStorageType::software: + result = compute_with_software_key(shared_secret, key.protected_secret, input, ephemeral_key); + break; + case KeyringStorageType::hardware: + result = compute_with_hardware_key(shared_secret, key.protected_secret, input, ephemeral_key); + break; + default: + result = ResponseResult::corrupted_entry; + break; + } + if (result != ResponseResult::success) + { + sodium_munlock(shared_secret, sizeof(shared_secret)); + return result; + } + // The proof is a BLAKE2b-256 hash over the following: // 1. Domain-separating constant. // 2. Shared DH secret, serving as a proof of posession of the secret key @@ -297,11 +466,14 @@ ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, Q crypto_generichash_update(&state, (const uchar *)domain_sep.constData(), domain_sep.size()); crypto_generichash_update(&state, shared_secret, sizeof(shared_secret)); crypto_generichash_update(&state, (const uchar *)ephemeral_key.constData(), ephemeral_key.size()); - crypto_generichash_update(&state, (const uchar *)public_key.constData(), public_key.size()); + crypto_generichash_update(&state, (const uchar *)key.public_key.constData(), key.public_key.size()); crypto_generichash_update(&state, (const uchar *)username.constData(), username.size()); crypto_generichash_final(&state, proof, sizeof(proof)); out.response = QByteArray((const char *)proof, (qsizetype)sizeof(proof)); + + sodium_munlock(shared_secret, sizeof(shared_secret)); + return ResponseResult::success; } diff --git a/src/keyring.h b/src/keyring.h index 81d97fc..42cf992 100644 --- a/src/keyring.h +++ b/src/keyring.h @@ -42,9 +42,32 @@ enum class ResponseResult derivation_failed, decryption_failed, bad_curve_point, + hardware_fault, + invalid_pin, + hardware_lockout, + incompatible_arguments, +}; + +enum class KeyringStorageType +{ + invalid, + software, + hardware, +}; + +struct KeyringKey +{ + KeyringStorageType type = KeyringStorageType::invalid; + QByteArray id; + QByteArray public_key; + QByteArray protected_secret; + QString name; }; int keyring_initialize(void); -int generate_key(QStringView name, const QByteArray &password); +int keyring_hardware_available(void); +int generate_software_key(QStringView name, const QByteArray &password); +int generate_hardware_key(QStringView name, const QByteArray &pin); +KeyringKey acquire_keyring_key(QByteArrayView key_id); void delete_key(const QByteArray &key_id); -ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, QByteArrayView ephemeral_key, QByteArrayView username, AuthResponse &out); +ResponseResult unlock_and_auth(const KeyringKey &key, QByteArrayView password, QByteArrayView ephemeral_key, QByteArrayView username, AuthResponse &out); diff --git a/src/tpm/tpm.h b/src/tpm/tpm.h new file mode 100644 index 0000000..7531ae2 --- /dev/null +++ b/src/tpm/tpm.h @@ -0,0 +1,33 @@ +// Copyright 2026 Osmium Sorcerer +// SPDX-License-Identifier: MIT + +#ifndef SOF_AO2CLIENT_TPM_TPM_H +#define SOF_AO2CLIENT_TPM_TPM_H + +#include <stdint.h> +#include <stddef.h> + +typedef enum { + STKB_SUCCESS, + STKB_ERROR, + STKB_TPM_UNAVAILABLE, + STKB_BAD_PIN, + STKB_AUTH_FAILURE, + STKB_LOCKOUT, +} stkb_rc; + +typedef struct { + const uint8_t *pin; + size_t pin_len; + const wchar_t *key_name; +} stkb_user_input; + +stkb_rc tpm2_init(void); + +stkb_rc tpm2_keygen(uint8_t *out_pub, uint8_t *out_buffer, size_t buffer_size, + size_t *written, stkb_user_input input); + +stkb_rc tpm2_compute_ss(uint8_t *ss, const uint8_t *blob, size_t blob_len, + const uint8_t *pk, stkb_user_input input); + +#endif /* SOF_AO2CLIENT_TPM_TPM_H */ diff --git a/src/tpm/tpm_cng.c b/src/tpm/tpm_cng.c new file mode 100644 index 0000000..1f764c7 --- /dev/null +++ b/src/tpm/tpm_cng.c @@ -0,0 +1,170 @@ +// Copyright 2026 Osmium Sorcerer +// SPDX-License-Identifier: MIT + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <ncrypt.h> +#include <rpc.h> + +#include "tpm.h" + +// Global handle for the Windows API. +static NCRYPT_PROV_HANDLE provider; + +// 9 characters for prefix, 36 for random UUID, and don't forget that null terminator. +// Note: this is count of wchar_t, not size. +static const size_t keyname_len = 46; + +stkb_rc tpm2_init(void) +{ + // "Microsoft Platform Crypto Provider" is the TPM. + if (NCryptOpenStorageProvider(&provider, MS_PLATFORM_CRYPTO_PROVIDER, 0) != ERROR_SUCCESS) + return STKB_TPM_UNAVAILABLE; + if (NCryptIsAlgSupported(provider, BCRYPT_ECDH_P256_ALGORITHM, 0) != ERROR_SUCCESS) + return STKB_TPM_UNAVAILABLE; + return STKB_SUCCESS; +} + + +static int generate_keyname(wchar_t *out) +{ + // UUID seems like the way on Windows. + UUID id; + RPC_WSTR wstr = NULL; + + if (UuidCreate(&id) != RPC_S_OK) + return 0; + + if (UuidToStringW(&id, &wstr) != RPC_S_OK) + return 0; + + wcscpy(out, L"SoF_Auth_"); + wcscpy(out + 9, wstr); + + RpcStringFreeW(&wstr); + + return 1; +} + +stkb_rc tpm2_keygen(uint8_t *out_pub, uint8_t *out_blob, size_t buffer_size, size_t *offset, + stkb_user_input input) +{ + NCRYPT_KEY_HANDLE key_handle = 0; + // Windows heavily abstracts the TPM and prevents you from acquiring the encrypted private structure. Instead, you + // have to use wide null-terminated strings as canonical key identifiers and refer to them by these names later. + // Yes, not even opaque byte arrays despite it handling "BLOBs." + wchar_t keyname[keyname_len]; + if (sizeof(keyname) > buffer_size) + return STKB_ERROR; + if (!generate_keyname(keyname)) + return STKB_ERROR; + + // One might think, when NULL is passed instead of a key name, the key will not actually be persistent. But it + // becomes impossible to export the key to be loaded later, even in an encrypted form (while NCryptExportKey + // documents NCRYPT_OPAQUETRANSPORT_BLOB, explicitly saying "Opaque BLOBs are not transferable and must be imported + // by using the same CSP," and NCRYPT_PROTECTED_KEY_BLOB, confusingly, also doesn't export an opaque fixed-TPM + // restricted key blob. So we are forced to use a unique (potentially user-facing) name as an internal specifier. + if (NCryptCreatePersistedKey(provider, &key_handle, BCRYPT_ECDH_P256_ALGORITHM, keyname, 0, 0) != ERROR_SUCCESS) + return STKB_ERROR; + + // Leave PINs unused, let the platform handle the input dialog. + NCRYPT_UI_POLICY ui_policy = { + .dwVersion = 1, + .dwFlags = NCRYPT_UI_FORCE_HIGH_PROTECTION_FLAG, + .pszFriendlyName = input.key_name, + .pszDescription = L"SoF authentication key", + }; + stkb_rc ret = STKB_ERROR; + if (NCryptSetProperty(key_handle, NCRYPT_UI_POLICY_PROPERTY, (PBYTE)&ui_policy, sizeof(ui_policy), 0) != ERROR_SUCCESS) + goto exit; + + // If the TPM is locked out, of all functions, FinalizeKey fails by returning NTE_INVALID_HANDLE. + // The other way it can happen (memory corruption aside) is if the user cancels the key creation + // dialog. You can't differentiate the two. + if (NCryptFinalizeKey(key_handle, 0) != ERROR_SUCCESS) { + ret = STKB_LOCKOUT; + goto exit; + } + + BCRYPT_ECCKEY_BLOB ecc_blob = { 0 }; + BYTE pub_buffer[sizeof(ecc_blob) + 32 * 2]; + DWORD pub_buffer_off = 0; + if (NCryptExportKey(key_handle, 0, BCRYPT_ECCPUBLIC_BLOB, NULL, pub_buffer, sizeof(pub_buffer), &pub_buffer_off, + 0) != ERROR_SUCCESS) + goto exit; + memcpy(&ecc_blob, pub_buffer, sizeof(ecc_blob)); + // Check sanity of the exported public key. + if (ecc_blob.dwMagic != BCRYPT_ECDH_PUBLIC_P256_MAGIC || ecc_blob.cbKey != 32) + goto exit; + // The x and y _should_ follow the blob (which is the beginning of the buffer) contiguously, each of cbKey (32) + // bytes. + memcpy(out_pub + 1, pub_buffer + sizeof(ecc_blob), 32); + // Encode parity of the last byte of y (both coordinates are big-endian). + if (pub_buffer[sizeof(ecc_blob) + 32 + 31] & 1) + out_pub[0] = 0x03; + else + out_pub[0] = 0x02; + + memcpy(out_blob, keyname, sizeof(keyname)); + *offset = sizeof(keyname); + + ret = STKB_SUCCESS; + +exit: + NCryptFreeObject(key_handle); + return ret; +} + +stkb_rc tpm2_compute_ss(uint8_t *ss, const uint8_t *blob, size_t blob_len, const uint8_t *pk, stkb_user_input input) +{ + (void)input; + // Because wchar_t is 16-byte-aligned, we can't cast a byte array, we have to do this redundant copy. + wchar_t keyname[keyname_len]; + if (blob_len != sizeof(keyname)) + return STKB_ERROR; + memcpy(keyname, blob, sizeof(keyname)); + stkb_rc ret = STKB_ERROR; + NCRYPT_KEY_HANDLE key_handle = 0; + NCRYPT_KEY_HANDLE pk_handle = 0; + NCRYPT_SECRET_HANDLE shared_point = 0; + if (NCryptOpenKey(provider, &key_handle, keyname, 0, 0) != ERROR_SUCCESS) + goto exit; + // The API expects us to do the same ceremony to import the key in its format instead of using it directly. + BCRYPT_ECCKEY_BLOB ecc_blob = { + .dwMagic = BCRYPT_ECDH_PUBLIC_P256_MAGIC, + .cbKey = 32, + }; + BYTE pub_buffer[sizeof(ecc_blob) + 32 * 2]; + memcpy(pub_buffer, &ecc_blob, sizeof(ecc_blob)); + memcpy(pub_buffer + sizeof(ecc_blob), pk, 64); + if (NCryptImportKey(provider, 0, BCRYPT_ECCPUBLIC_BLOB, NULL, &pk_handle, pub_buffer, sizeof(pub_buffer), 0) != ERROR_SUCCESS) + goto exit; + // Here it can also fail due to cancelation of dialog, too many failed attempts, or a TPM lockout. + if (NCryptSecretAgreement(key_handle, pk_handle, &shared_point, 0) != ERROR_SUCCESS) { + ret = STKB_LOCKOUT; + goto exit; + } + // You cannot extract the x coordinate of the shared point either, you have to derive the key. Even if you want to + // directly use the secret. But fine, apparently RAW_SECRET is a cryptographic key derivation function, which is + // some unrecognized cryptographic genius. We'll use it to get our affine big-endian x coordinate, the canonical + // ECDH shared secret. If you specify the size to be 32, it will only output the x coordinate, after all, that's + // what a point is: 32 bytes of x, then 32 bytes of y, no padding, no leading bytes, no headers. Right? + BYTE secret[32]; + DWORD written = 0; + if (NCryptDeriveKey(shared_point, BCRYPT_KDF_RAW_SECRET, NULL, secret, sizeof(secret), &written, 0) != ERROR_SUCCESS) + goto exit; + // Of course it wouldn't be that easy. Even if we guessed the behavior of this function with respect to the buffer + // size you pass to it, CNG API decides that it's a good idea to output point coordinates in little-endian order, + // opposite of how the standard defines it (and what every other implementation correctly does, including the + // Platform Crypto backend that CNG relies on, and CNG's own NCryptExportKey). + for (size_t i = 0; i < sizeof(secret); ++i) + ss[i] = secret[sizeof(secret) - 1 - i]; + + ret = STKB_SUCCESS; + +exit: + NCryptFreeObject(key_handle); + NCryptFreeObject(pk_handle); + NCryptFreeObject(shared_point); + return ret; +} diff --git a/src/tpm/tpm_stub.c b/src/tpm/tpm_stub.c new file mode 100644 index 0000000..4e2d2c0 --- /dev/null +++ b/src/tpm/tpm_stub.c @@ -0,0 +1,28 @@ +#include "tpm.h" + +stkb_rc tpm2_init(void) +{ + return STKB_TPM_UNAVAILABLE; +} + +stkb_rc tpm2_keygen(uint8_t *out_pub, uint8_t *out_buffer, size_t buffer_size, + size_t *written, stkb_user_input input) +{ + (void)out_pub; + (void)out_buffer; + (void)buffer_size; + (void)written; + (void)input; + return STKB_TPM_UNAVAILABLE; +} + +stkb_rc tpm2_compute_ss(uint8_t *ss, const uint8_t *blob, size_t blob_len, + const uint8_t *pk, stkb_user_input input) +{ + (void)ss; + (void)blob; + (void)blob_len; + (void)pk; + (void)input; + return STKB_TPM_UNAVAILABLE; +} diff --git a/src/tpm/tpm_tss.c b/src/tpm/tpm_tss.c new file mode 100644 index 0000000..880fa32 --- /dev/null +++ b/src/tpm/tpm_tss.c @@ -0,0 +1,244 @@ +// Copyright 2026 Osmium Sorcerer +// SPDX-License-Identifier: MIT + +#include <string.h> + +#include <tss2/tss2_esys.h> +#include <tss2/tss2_mu.h> + +#include "tpm.h" + +// Storage Key: asymmetric scheme is NULL, symmetric is defined and shall +// use CFB mode. The key is restricted. +static const TPM2B_PUBLIC primary_template = { + .publicArea = { + .type = TPM2_ALG_ECC, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = + TPMA_OBJECT_FIXEDTPM | + TPMA_OBJECT_FIXEDPARENT | + TPMA_OBJECT_SENSITIVEDATAORIGIN | + TPMA_OBJECT_USERWITHAUTH | + TPMA_OBJECT_RESTRICTED | + TPMA_OBJECT_DECRYPT, + .parameters.eccDetail = { + .symmetric = { + .algorithm = TPM2_ALG_AES, + .keyBits.aes = 128, + .mode.aes = TPM2_ALG_CFB, + }, + .scheme.scheme = TPM2_ALG_NULL, + .curveID = TPM2_ECC_NIST_P256, + .kdf.scheme = TPM2_ALG_NULL, + }, + }, +}; + +// Symmetric algorithm is NULL for a nonrestricted key. This is the template +// for actual key exchange keys under the primary storage key, accessed with a +// user authentication value. +// There is TPM2_ALG_ECDH scheme, but it requires KDF. +static const TPM2B_PUBLIC ecdh_key_template = { + .publicArea = { + .type = TPM2_ALG_ECC, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = + TPMA_OBJECT_FIXEDTPM | + TPMA_OBJECT_FIXEDPARENT | + TPMA_OBJECT_SENSITIVEDATAORIGIN | + TPMA_OBJECT_USERWITHAUTH | + TPMA_OBJECT_DECRYPT, + .parameters.eccDetail = { + .symmetric.algorithm = TPM2_ALG_NULL, + .scheme.scheme = TPM2_ALG_NULL, + .curveID = TPM2_ECC_NIST_P256, + .kdf.scheme = TPM2_ALG_NULL, + }, + }, +}; + +static const TPM2B_SENSITIVE_CREATE empty_in_sensitive = { 0 }; +static const TPM2B_DATA empty_outside_info = { 0 }; +static const TPML_PCR_SELECTION empty_creation_pcr = { 0 }; + +// Make sure TPM 2.0 is present and working correctly before using it. +stkb_rc tpm2_init(void) +{ + int ret = 0; + ESYS_CONTEXT *ctx; + if (Esys_Initialize(&ctx, NULL, NULL) != TSS2_RC_SUCCESS) + return STKB_TPM_UNAVAILABLE; + // Simple self-test. Tests only what's necessary instead of all internal + // functions and operations, doesn't disrupt the system. + ret = Esys_SelfTest(ctx, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_NO); + Esys_Finalize(&ctx); + if (ret != TSS2_RC_SUCCESS) + return STKB_TPM_UNAVAILABLE; + return STKB_SUCCESS; +} + +static TSS2_RC tpm2_setup_primary(ESYS_CONTEXT **ctx, ESYS_TR *primary_handle) +{ + TSS2_RC rc = Esys_Initialize(ctx, NULL, NULL); + if (rc != TSS2_RC_SUCCESS) + return rc; + rc = Esys_CreatePrimary(*ctx, ESYS_TR_RH_OWNER, ESYS_TR_PASSWORD, + ESYS_TR_NONE, ESYS_TR_NONE, &empty_in_sensitive, + &primary_template, &empty_outside_info, + &empty_creation_pcr, primary_handle, NULL, NULL, + NULL, NULL); + if (rc != TSS2_RC_SUCCESS) + Esys_Finalize(ctx); + return rc; +} + +static stkb_rc decode_tpm_rc(TSS2_RC tpm_rc) +{ + // Check that the code is from the TPM itself rather than ESAPI, otherwise + // return a generic error as we're not interested in details. + if ((tpm_rc & TSS2_RC_LAYER_MASK) != TSS2_TPM_RC_LAYER) + return STKB_ERROR; + + // Format-One return codes are composite and in particular have an added + // parameter value that is irrelevant to the nature of the error, clear it. + if (tpm_rc & TPM2_RC_FMT1) + tpm_rc &= ~TPM2_RC_N_MASK; + + switch (tpm_rc) { + case TPM2_RC_BAD_AUTH: + case TPM2_RC_AUTH_FAIL: + return STKB_AUTH_FAILURE; + case TPM2_RC_LOCKOUT: + return STKB_LOCKOUT; + default: + return STKB_ERROR; + } +} + +stkb_rc tpm2_keygen(uint8_t *out_pub, uint8_t *out_buffer, size_t buffer_size, + size_t *written, stkb_user_input input) +{ + if (input.pin_len > TPM2_SHA256_DIGEST_SIZE) + return STKB_BAD_PIN; + TPM2B_SENSITIVE_CREATE in_sensitive = { 0 }; + memcpy(in_sensitive.sensitive.userAuth.buffer, input.pin, input.pin_len); + in_sensitive.sensitive.userAuth.size = (UINT16)input.pin_len; + ESYS_CONTEXT *ctx; + ESYS_TR primary_handle; + stkb_rc ret = STKB_ERROR; + if (tpm2_setup_primary(&ctx, &primary_handle) != TSS2_RC_SUCCESS) + return ret; + TPM2B_PRIVATE *ecdh_priv = NULL; + TPM2B_PUBLIC *ecdh_pub = NULL; + TSS2_RC rc = Esys_Create(ctx, primary_handle, ESYS_TR_PASSWORD, + ESYS_TR_NONE, ESYS_TR_NONE, &in_sensitive, + &ecdh_key_template, &empty_outside_info, + &empty_creation_pcr, &ecdh_priv, &ecdh_pub, NULL, + NULL, NULL); + if (rc != TSS2_RC_SUCCESS) { + ret = decode_tpm_rc(rc); + goto exit; + } + + // TPM 2.0 Library, Part 1, 44.5.3 Padding: + // + // > In ECC points returned by the TPM, the x and y values, if non-empty, + // > are required to be the size of their associated curve (e.g., 32 bytes + // > for NIST P-256). + if (ecdh_pub->publicArea.unique.ecc.x.size != 32 || + ecdh_pub->publicArea.unique.ecc.y.size != 32) + goto exit; + + // Compress the public key: knowing x, it's enough to only encode parity of + // y to fully reconstruct the point. + if (ecdh_pub->publicArea.unique.ecc.y.buffer[31] & 1) + out_pub[0] = 0x03; + else + out_pub[0] = 0x02; + memcpy(out_pub + 1, ecdh_pub->publicArea.unique.ecc.x.buffer, 32); + + // Serialize both structures, we'll need them for Load. + size_t off = 0; + if (Tss2_MU_TPM2B_PRIVATE_Marshal(ecdh_priv, out_buffer, buffer_size, &off) != + TSS2_RC_SUCCESS) + goto exit; + if (Tss2_MU_TPM2B_PUBLIC_Marshal(ecdh_pub, out_buffer, buffer_size - off, + &off) != TSS2_RC_SUCCESS) + goto exit; + + *written = off; + ret = STKB_SUCCESS; + +exit: + Esys_Free(ecdh_priv); + Esys_Free(ecdh_pub); + Esys_FlushContext(ctx, primary_handle); + Esys_Finalize(&ctx); + return ret; +} + +// pk is an uncompressed 64-byte point (x || y) of the peer. +stkb_rc tpm2_compute_ss(uint8_t *ss, const uint8_t *blob, size_t blob_len, + const uint8_t *pk, stkb_user_input input) +{ + if (input.pin_len > TPM2_SHA256_DIGEST_SIZE) + return STKB_BAD_PIN; + TPM2B_AUTH auth_value = { 0 }; + memcpy(auth_value.buffer, input.pin, input.pin_len); + auth_value.size = (UINT16)input.pin_len; + + ESYS_CONTEXT *ctx; + ESYS_TR primary_handle; + stkb_rc ret = STKB_ERROR; + if (tpm2_setup_primary(&ctx, &primary_handle) != TSS2_RC_SUCCESS) + return ret; + + ESYS_TR ecdh_key_handle = ESYS_TR_NONE; + TPM2B_PRIVATE ecdh_priv; + TPM2B_PUBLIC ecdh_pub; + TPM2B_ECC_POINT *shared_point = NULL; + size_t off = 0; + if (Tss2_MU_TPM2B_PRIVATE_Unmarshal(blob, blob_len, &off, &ecdh_priv) != + TSS2_RC_SUCCESS) + goto exit; + if (Tss2_MU_TPM2B_PUBLIC_Unmarshal(blob, blob_len, &off, &ecdh_pub) != + TSS2_RC_SUCCESS) + goto exit; + + TSS2_RC rc = Esys_Load(ctx, primary_handle, ESYS_TR_PASSWORD, ESYS_TR_NONE, + ESYS_TR_NONE, &ecdh_priv, &ecdh_pub, + &ecdh_key_handle); + if (rc != TSS2_RC_SUCCESS) { + ret = decode_tpm_rc(rc); + goto exit; + } + + if (Esys_TR_SetAuth(ctx, ecdh_key_handle, &auth_value) != TSS2_RC_SUCCESS) + goto exit; + + TPM2B_ECC_POINT in_point; + in_point.point.x.size = 32; + in_point.point.y.size = 32; + memcpy(in_point.point.x.buffer, pk, 32); + memcpy(in_point.point.y.buffer, pk + 32, 32); + rc = Esys_ECDH_ZGen(ctx, ecdh_key_handle, ESYS_TR_PASSWORD, ESYS_TR_NONE, + ESYS_TR_NONE, &in_point, &shared_point); + if (rc != TSS2_RC_SUCCESS) { + ret = decode_tpm_rc(rc); + goto exit; + } + + // Deliver the x coordinate of the shared point. + memcpy(ss, shared_point->point.x.buffer, 32); + + ret = STKB_SUCCESS; + +exit: + Esys_Free(shared_point); + if (primary_handle != ESYS_TR_NONE) + Esys_FlushContext(ctx, primary_handle); + if (ecdh_key_handle != ESYS_TR_NONE) + Esys_FlushContext(ctx, ecdh_key_handle); + Esys_Finalize(&ctx); + return ret; +} |
