From 24dd4b2e691f30fb79b1643f93892401da91d824 Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Fri, 15 May 2026 10:30:46 +0200 Subject: [PATCH] Add PIN protection for TPM in windows and linux. Signed-off-by: Pol Henarejos --- src/otp/otp_linux.c | 160 +++++++++++++++++++++++---- src/otp/otp_windows.c | 246 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 382 insertions(+), 24 deletions(-) diff --git a/src/otp/otp_linux.c b/src/otp/otp_linux.c index c6ba45d..c6a12a4 100644 --- a/src/otp/otp_linux.c +++ b/src/otp/otp_linux.c @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include "picokeys.h" #include "otp_platform.h" @@ -37,6 +39,51 @@ #define OTP_LINUX_DEFAULT_TPM_HANDLE "0x81010001" #define OTP_LINUX_DEFAULT_PEER_KEY_FILE ".config/pico-novus/otp_peer_p256.bin" +static int read_tpm_pin_prompt(const char *prompt, char *pin_out, size_t pin_out_size) { + const char *pin_env = getenv("PICO_NOVUS_TPM_PIN"); + if (!pin_out || pin_out_size < 2) { + return -1; + } + if (pin_env && pin_env[0] != '\0') { + size_t n = strlen(pin_env); + if (n >= pin_out_size) { + return -1; + } + memcpy(pin_out, pin_env, n + 1); + return 0; + } + if (!isatty(STDIN_FILENO)) { + fprintf(stderr, "[otp-linux] No TTY for PIN prompt; set PICO_NOVUS_TPM_PIN\n"); + return -1; + } + struct termios oldt; + struct termios newt; + if (tcgetattr(STDIN_FILENO, &oldt) != 0) { + return -1; + } + newt = oldt; + newt.c_lflag &= ~(ECHO); + fprintf(stderr, "%s", prompt ? prompt : "Enter TPM key PIN: "); + fflush(stderr); + if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) != 0) { + return -1; + } + if (!fgets(pin_out, (int)pin_out_size, stdin)) { + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + fprintf(stderr, "\n"); + return -1; + } + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + fprintf(stderr, "\n"); + { + size_t n = strlen(pin_out); + if (n > 0 && pin_out[n - 1] == '\n') { + pin_out[n - 1] = '\0'; + } + } + return (pin_out[0] != '\0') ? 0 : -1; +} + static int derive_secp256k1_privkey_from_secret(const uint8_t *secret, size_t secret_len, uint8_t out_key32[32]) { int rc = -1; uint8_t digest[32]; @@ -278,7 +325,7 @@ cleanup: return rc; } -static int load_or_create_tpm_p256_key(ESYS_CONTEXT *esys, TPM2_HANDLE handle, ESYS_TR *key_out) { +static int load_or_create_tpm_p256_key(ESYS_CONTEXT *esys, TPM2_HANDLE handle, const char *pin, ESYS_TR *key_out, int *created_out) { TSS2_RC rc; ESYS_TR key = ESYS_TR_NONE; ESYS_TR transient = ESYS_TR_NONE; @@ -291,16 +338,48 @@ static int load_or_create_tpm_p256_key(ESYS_CONTEXT *esys, TPM2_HANDLE handle, E TPM2B_CREATION_DATA *creation_data = NULL; TPM2B_DIGEST *creation_hash = NULL; TPMT_TK_CREATION *creation_ticket = NULL; + TPMS_CAPABILITY_DATA *cap_data = NULL; + TPMI_YES_NO more_data = TPM2_NO; + TPM2B_AUTH auth = {0}; if (!esys || !key_out) { return -1; } *key_out = ESYS_TR_NONE; + if (created_out) { + *created_out = 0; + } + if (pin && pin[0] != '\0') { + size_t pin_len = strlen(pin); + if (pin_len > sizeof(auth.buffer)) { + return -1; + } + auth.size = (UINT16)pin_len; + memcpy(auth.buffer, pin, pin_len); + } - rc = Esys_TR_FromTPMPublic(esys, handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &key); - if (rc == TSS2_RC_SUCCESS) { + rc = Esys_GetCapability(esys, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_CAP_HANDLES, handle, 1, &more_data, &cap_data); + if (rc != TSS2_RC_SUCCESS) { + fprintf(stderr, "[otp-linux] Esys_GetCapability failed while checking key handle: 0x%x\n", rc); + goto cleanup; + } + if (cap_data && cap_data->data.handles.count > 0 && cap_data->data.handles.handle[0] == handle) { + rc = Esys_TR_FromTPMPublic(esys, handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &key); + if (rc != TSS2_RC_SUCCESS) { + fprintf(stderr, "[otp-linux] Esys_TR_FromTPMPublic failed for existing key: 0x%x\n", rc); + goto cleanup; + } + if (auth.size > 0) { + rc = Esys_TR_SetAuth(esys, key, &auth); + if (rc != TSS2_RC_SUCCESS) { + Esys_TR_Close(esys, &key); + key = ESYS_TR_NONE; + goto cleanup; + } + } *key_out = key; - return 0; + key = ESYS_TR_NONE; + goto cleanup; } in_public.publicArea.type = TPM2_ALG_ECC; @@ -311,6 +390,9 @@ static int load_or_create_tpm_p256_key(ESYS_CONTEXT *esys, TPM2_HANDLE handle, E in_public.publicArea.parameters.eccDetail.curveID = TPM2_ECC_NIST_P256; in_public.publicArea.parameters.eccDetail.kdf.scheme = TPM2_ALG_NULL; + if (auth.size > 0) { + in_sensitive.sensitive.userAuth = auth; + } rc = Esys_CreatePrimary(esys, ESYS_TR_RH_OWNER, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, &in_sensitive, &in_public, &outside_info, &creation_pcr, &transient, &out_public, &creation_data, &creation_hash, &creation_ticket); if (rc != TSS2_RC_SUCCESS || transient == ESYS_TR_NONE) { fprintf(stderr, "[otp-linux] Esys_CreatePrimary failed while provisioning TPM key: 0x%x\n", rc); @@ -331,23 +413,43 @@ static int load_or_create_tpm_p256_key(ESYS_CONTEXT *esys, TPM2_HANDLE handle, E } if (persisted != ESYS_TR_NONE) { - *key_out = persisted; + Esys_TR_Close(esys, &persisted); persisted = ESYS_TR_NONE; } - else { - rc = Esys_TR_FromTPMPublic(esys, handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, key_out); + rc = Esys_TR_FromTPMPublic(esys, handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, key_out); + if (rc != TSS2_RC_SUCCESS) { + fprintf(stderr, "[otp-linux] Persisted key created but reload failed: 0x%x\n", rc); + *key_out = ESYS_TR_NONE; + goto cleanup; + } + if (auth.size > 0) { + rc = Esys_TR_SetAuth(esys, *key_out, &auth); if (rc != TSS2_RC_SUCCESS) { - fprintf(stderr, "[otp-linux] Persisted key created but reload failed: 0x%x\n", rc); + Esys_TR_Close(esys, key_out); *key_out = ESYS_TR_NONE; goto cleanup; } } + if (created_out) { + *created_out = 1; + } cleanup: - if (creation_ticket) Esys_Free(creation_ticket); - if (creation_hash) Esys_Free(creation_hash); - if (creation_data) Esys_Free(creation_data); - if (out_public) Esys_Free(out_public); + if (cap_data) { + Esys_Free(cap_data); + } + if (creation_ticket) { + Esys_Free(creation_ticket); + } + if (creation_hash) { + Esys_Free(creation_hash); + } + if (creation_data) { + Esys_Free(creation_data); + } + if (out_public) { + Esys_Free(out_public); + } if (persisted != ESYS_TR_NONE) { Esys_TR_Close(esys, &persisted); } @@ -367,6 +469,8 @@ static int linux_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { uint8_t peer_priv[32] = {0}; uint8_t ecdh_secret[132] = {0}; size_t ecdh_secret_len = 0; + char pin[128] = {0}; + int created_now = 0; int rc_out = -1; TSS2_RC rc; @@ -402,13 +506,23 @@ static int linux_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { fprintf(stderr, "[otp-linux] Esys_Initialize failed: 0x%x\n", rc); goto cleanup; } - - { - unsigned long handle_num = strtoul(handle_hex, NULL, 0); - if (load_or_create_tpm_p256_key(esys, (TPM2_HANDLE)handle_num, &tpm_key) != 0) { - fprintf(stderr, "[otp-linux] Cannot load/create persistent TPM key at handle: %s\n", handle_hex); + unsigned long handle_num = strtoul(handle_hex, NULL, 0); + if (read_tpm_pin_prompt("Enter or set TPM key PIN: ", pin, sizeof(pin)) != 0) { + fprintf(stderr, "[otp-linux] PIN is required to unlock/provision TPM key\n"); + goto cleanup; + } + if (load_or_create_tpm_p256_key(esys, (TPM2_HANDLE)handle_num, pin, &tpm_key, &created_now) != 0) { + fprintf(stderr, "[otp-linux] Cannot load/create persistent TPM key at handle: %s\n", handle_hex); + goto cleanup; + } + if (created_now && !getenv("PICO_NOVUS_TPM_PIN") && isatty(STDIN_FILENO)) { + char confirm[128] = {0}; + if (read_tpm_pin_prompt("Confirm TPM key PIN: ", confirm, sizeof(confirm)) != 0 || strcmp(pin, confirm) != 0) { + fprintf(stderr, "[otp-linux] PIN confirmation mismatch\n"); + memset(confirm, 0, sizeof(confirm)); goto cleanup; } + memset(confirm, 0, sizeof(confirm)); } rc = Esys_ReadPublic(esys, tpm_key, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &tpm_pub, NULL, NULL); @@ -434,6 +548,9 @@ static int linux_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { rc = Esys_ECDH_ZGen(esys, tpm_key, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, &peer_pub, &z_point); if (rc != TSS2_RC_SUCCESS || !z_point) { + if (rc == TPM2_RC_AUTH_FAIL || rc == TPM2_RC_BAD_AUTH) { + fprintf(stderr, "[otp-linux] TPM key auth failed (wrong PIN?)\n"); + } fprintf(stderr, "[otp-linux] Esys_ECDH_ZGen failed: 0x%x\n", rc); goto cleanup; } @@ -456,8 +573,13 @@ static int linux_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { rc_out = 0; cleanup: - if (z_point) Esys_Free(z_point); - if (tpm_pub) Esys_Free(tpm_pub); + memset(pin, 0, sizeof(pin)); + if (z_point) { + Esys_Free(z_point); + } + if (tpm_pub) { + Esys_Free(tpm_pub); + } if (esys) { if (tpm_key != ESYS_TR_NONE) { Esys_TR_Close(esys, &tpm_key); diff --git a/src/otp/otp_windows.c b/src/otp/otp_windows.c index 2990272..1a019ac 100644 --- a/src/otp/otp_windows.c +++ b/src/otp/otp_windows.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "mbedtls/bignum.h" #include "mbedtls/ecp.h" @@ -62,6 +63,168 @@ static const char *ncrypt_status_hint(SECURITY_STATUS st) { } } +static int read_tpm_pin_prompt(const char *prompt, char *pin_out, size_t pin_out_size) { + const char *pin_env = getenv("PICO_NOVUS_TPM_PIN"); + if (!pin_out || pin_out_size < 2) { + return -1; + } + if (pin_env && pin_env[0] != '\0') { + size_t n = strlen(pin_env); + if (n >= pin_out_size) { + return -1; + } + memcpy(pin_out, pin_env, n + 1); + return 0; + } + + if (!_isatty(_fileno(stdin))) { + fprintf(stderr, "[win-tpm] No TTY for PIN prompt; set PICO_NOVUS_TPM_PIN\n"); + return -1; + } + HANDLE h_in = GetStdHandle(STD_INPUT_HANDLE); + DWORD old_mode = 0; + DWORD new_mode = 0; + + if (h_in == INVALID_HANDLE_VALUE || !GetConsoleMode(h_in, &old_mode)) { + return -1; + } + + new_mode = old_mode & (~ENABLE_ECHO_INPUT); + fprintf(stderr, "%s", prompt ? prompt : "Enter TPM key PIN: "); + fflush(stderr); + + if (!SetConsoleMode(h_in, new_mode)) { + return -1; + } + if (!fgets(pin_out, (int)pin_out_size, stdin)) { + SetConsoleMode(h_in, old_mode); + fprintf(stderr, "\n"); + return -1; + } + SetConsoleMode(h_in, old_mode); + fprintf(stderr, "\n"); + size_t n = strlen(pin_out); + if (n > 0 && pin_out[n - 1] == '\n') { + pin_out[n - 1] = '\0'; + } + if (n > 1 && pin_out[n - 2] == '\r') { + pin_out[n - 2] = '\0'; + } + return (pin_out[0] != '\0') ? 0 : -1; +} + +static int key_exists_by_name(NCRYPT_PROV_HANDLE prov, LPCWSTR key_name, int *exists_out) { + SECURITY_STATUS st; + NCRYPT_KEY_HANDLE key = 0; + + if (!exists_out) { + return -1; + } + *exists_out = 0; + + st = NCryptOpenKey(prov, &key, key_name, 0, 0); + if (st == ERROR_SUCCESS) { + *exists_out = 1; + NCryptFreeObject(key); + return 0; + } + if (st == NTE_BAD_KEYSET) { + return 0; + } + return win_err("NCryptOpenKey(exists)", st); +} + +static int delete_persisted_key_if_exists(NCRYPT_PROV_HANDLE prov, LPCWSTR key_name) { + SECURITY_STATUS st; + NCRYPT_KEY_HANDLE key = 0; + + st = NCryptOpenKey(prov, &key, key_name, 0, 0); + if (st == NTE_BAD_KEYSET) { + return 0; + } + if (st != ERROR_SUCCESS) { + return win_err("NCryptOpenKey(delete)", st); + } + + st = NCryptDeleteKey(key, 0); + if (st != ERROR_SUCCESS) { + NCryptFreeObject(key); + return win_err("NCryptDeleteKey", st); + } + + printf("[win-tpm] Deleted key '%ls'\n", key_name); + return 0; +} + +static int set_tpm_usage_pin(NCRYPT_KEY_HANDLE key, const char *pin, const char *ctx) { + SECURITY_STATUS st; + int wlen; + wchar_t wpin[128]; + + if (!pin || pin[0] == '\0') { + return 0; + } + + memset(wpin, 0, sizeof(wpin)); + wlen = MultiByteToWideChar(CP_UTF8, 0, pin, -1, wpin, (int)(sizeof(wpin) / sizeof(wpin[0]))); + if (wlen <= 1) { + fprintf(stderr, "[win-tpm] invalid PIN encoding\n"); + return -1; + } + + /* PCP usage auth is expected as WCHAR string (including terminator). */ + st = NCryptSetProperty( + key, + NCRYPT_PCP_USAGEAUTH_PROPERTY, + (PBYTE)wpin, + (DWORD)(wlen * sizeof(wchar_t)), + 0 + ); + SecureZeroMemory(wpin, sizeof(wpin)); + if (st != ERROR_SUCCESS) { + if (st == NTE_NOT_SUPPORTED) { + fprintf(stderr, "[win-tpm] TPM provider does not support PIN auth property on this machine\n"); + return 1; + } + return win_err(ctx, st); + } + return 0; +} + +static int probe_tpm_native_pin_support(NCRYPT_PROV_HANDLE prov) { + SECURITY_STATUS st; + NCRYPT_KEY_HANDLE probe_key = 0; + const wchar_t probe_name[] = L"pico_novus_pin_probe_tmp"; + const wchar_t probe_pin[] = L"0"; + + st = NCryptCreatePersistedKey(prov, &probe_key, NCRYPT_ECDH_P256_ALGORITHM, probe_name, 0, NCRYPT_OVERWRITE_KEY_FLAG); + if (st != ERROR_SUCCESS) { + return -1; + } + + st = NCryptSetProperty( + probe_key, + NCRYPT_PCP_USAGEAUTH_PROPERTY, + (PBYTE)probe_pin, + (DWORD)(sizeof(probe_pin)), + 0 + ); + if (st == NTE_NOT_SUPPORTED) { + NCryptDeleteKey(probe_key, 0); + return 0; + } + if (st != ERROR_SUCCESS) { + NCryptDeleteKey(probe_key, 0); + return -1; + } + + st = NCryptDeleteKey(probe_key, 0); + if (st != ERROR_SUCCESS) { + return -1; + } + return 1; +} + static int derive_secp256k1_privkey_from_secret(const uint8_t *secret, size_t secret_len, uint8_t out_key32[32]) { int rc = -1; uint8_t digest[32]; @@ -116,14 +279,22 @@ cleanup: return rc; } -static int open_or_create_persisted_ecdh_p256_key(NCRYPT_PROV_HANDLE prov, LPCWSTR key_name, NCRYPT_KEY_HANDLE *out_key) { +static int open_or_create_persisted_ecdh_p256_key(NCRYPT_PROV_HANDLE prov, LPCWSTR key_name, const char *pin, NCRYPT_KEY_HANDLE *out_key, int *created_out) { SECURITY_STATUS st; NCRYPT_KEY_HANDLE key = 0; + DWORD pin_len = 0; + int pin_rc = 0; if (!out_key) { return -1; } *out_key = 0; + if (created_out) { + *created_out = 0; + } + if (pin && pin[0] != '\0') { + pin_len = (DWORD)strlen(pin); + } st = NCryptOpenKey(prov, &key, key_name, 0, 0); if (st == NTE_BAD_KEYSET) { @@ -131,12 +302,25 @@ static int open_or_create_persisted_ecdh_p256_key(NCRYPT_PROV_HANDLE prov, LPCWS if (st != ERROR_SUCCESS) { return win_err("NCryptCreatePersistedKey", st); } + if (pin_len > 0) { + pin_rc = set_tpm_usage_pin(key, pin, "NCryptSetProperty(NCRYPT_PCP_USAGEAUTH_PROPERTY/create)"); + if (pin_rc == 1) { + fprintf(stderr, "[win-tpm] PIN protection unsupported by TPM provider; continuing without TPM-native PIN\n"); + } + else if (pin_rc != 0) { + NCryptFreeObject(key); + return -1; + } + } st = NCryptFinalizeKey(key, 0); if (st != ERROR_SUCCESS) { NCryptFreeObject(key); return win_err("NCryptFinalizeKey", st); } printf("[win-tpm] Created key '%ls'\n", key_name); + if (created_out) { + *created_out = 1; + } } else if (st != ERROR_SUCCESS) { return win_err("NCryptOpenKey", st); @@ -145,6 +329,17 @@ static int open_or_create_persisted_ecdh_p256_key(NCRYPT_PROV_HANDLE prov, LPCWS printf("[win-tpm] Using existing key '%ls'\n", key_name); } + if (pin_len > 0) { + pin_rc = set_tpm_usage_pin(key, pin, "NCryptSetProperty(NCRYPT_PCP_USAGEAUTH_PROPERTY/open)"); + if (pin_rc == 1) { + fprintf(stderr, "[win-tpm] PIN protection unsupported by TPM provider; continuing without TPM-native PIN\n"); + } + else if (pin_rc != 0) { + NCryptFreeObject(key); + return -1; + } + } + *out_key = key; return 0; } @@ -162,6 +357,9 @@ static int windows_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { DWORD raw_secret_len = 0; PBYTE peer_pub_blob = NULL; DWORD peer_pub_blob_len = 0; + char pin[128] = {0}; + int key_exists = 0; + int pin_supported = 0; st = NCryptOpenStorageProvider(&tpm_prov, MS_PLATFORM_CRYPTO_PROVIDER, 0); if (st != ERROR_SUCCESS) { @@ -174,10 +372,10 @@ static int windows_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { return win_err("NCryptOpenStorageProvider(MS_KEY_STORAGE_PROVIDER)", st); } printf("[win-tpm] Falling back to software KSP (MS_KEY_STORAGE_PROVIDER)\n"); - if (open_or_create_persisted_ecdh_p256_key(sw_prov, SW_LOCAL_NAME, &tpm_key) != 0) { + if (open_or_create_persisted_ecdh_p256_key(sw_prov, SW_LOCAL_NAME, NULL, &tpm_key, NULL) != 0) { goto cleanup; } - if (open_or_create_persisted_ecdh_p256_key(sw_prov, SW_PEER_NAME, &sw_peer_key) != 0) { + if (open_or_create_persisted_ecdh_p256_key(sw_prov, SW_PEER_NAME, NULL, &sw_peer_key, NULL) != 0) { goto cleanup; } st = NCryptSecretAgreement(tpm_key, sw_peer_key, &secret, 0); @@ -218,10 +416,47 @@ static int windows_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { goto cleanup; } - if (open_or_create_persisted_ecdh_p256_key(tpm_prov, TPM_KEY_NAME, &tpm_key) != 0) { + pin_supported = probe_tpm_native_pin_support(tpm_prov); + if (pin_supported < 0) { + fprintf(stderr, "[win-tpm] Cannot determine TPM-native PIN support; continuing without PIN\n"); + pin_supported = 0; + } + else if (pin_supported == 0) { + fprintf(stderr, "[win-tpm] TPM provider does not support native PIN auth; continuing without PIN\n"); + } + + if (key_exists_by_name(tpm_prov, TPM_KEY_NAME, &key_exists) != 0) { goto cleanup; } - if (open_or_create_persisted_ecdh_p256_key(sw_prov, SW_PEER_NAME, &sw_peer_key) != 0) { + if (pin_supported) { + if (!key_exists) { + if (read_tpm_pin_prompt("Set TPM key PIN: ", pin, sizeof(pin)) != 0) { + fprintf(stderr, "[win-tpm] PIN is required to provision TPM key\n"); + goto cleanup; + } + if (!getenv("PICO_NOVUS_TPM_PIN") && _isatty(_fileno(stdin))) { + char confirm[128] = {0}; + if (read_tpm_pin_prompt("Confirm TPM key PIN: ", confirm, sizeof(confirm)) != 0 || strcmp(pin, confirm) != 0) { + fprintf(stderr, "[win-tpm] PIN confirmation mismatch\n"); + SecureZeroMemory(confirm, sizeof(confirm)); + goto cleanup; + } + SecureZeroMemory(confirm, sizeof(confirm)); + } + } + else { + if (read_tpm_pin_prompt("Enter TPM key PIN: ", pin, sizeof(pin)) != 0) { + fprintf(stderr, "[win-tpm] PIN is required to unlock TPM key\n"); + goto cleanup; + } + } + } + + if (open_or_create_persisted_ecdh_p256_key(tpm_prov, TPM_KEY_NAME, pin_supported ? pin : NULL, &tpm_key, NULL) != 0) { + goto cleanup; + } + + if (open_or_create_persisted_ecdh_p256_key(sw_prov, SW_PEER_NAME, NULL, &sw_peer_key, NULL) != 0) { goto cleanup; } @@ -279,6 +514,7 @@ static int windows_tpm_vault_load_or_create_key(uint8_t out_key32[32]) { rc = 0; cleanup: + SecureZeroMemory(pin, sizeof(pin)); if (peer_pub_blob) { SecureZeroMemory(peer_pub_blob, peer_pub_blob_len); free(peer_pub_blob);