diff --git a/NEWS b/NEWS index ecae25fb..312bf8b6 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,10 @@ NEWS for Libp11 -- History of user visible changes New in 0.4.19; unreleased +* Added the opt-in "no_login_cache" provider parameter, which releases the + token login session when a key is freed instead of caching it until exit. + Avoids session-slot exhaustion on tokens with a hard session limit (e.g. + YubiHSM 2) driven by repeated short-lived signing processes. (Dante Melo) * Define LIBP11_VERSION_NUMBER (Michał Trojnara) * Added PKCS#11 provider support for OpenSSL 4.x. (Małgorzata Olszówka) diff --git a/README.md b/README.md index dad979ad..3503329f 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ activate = 1 Some parameters can be overridden using environment variables: `OPENSSL_MODULES`, `PKCS11_MODULE_PATH`, `PKCS11_DEBUG_LEVEL`, -`PKCS11_FORCE_LOGIN`, `PKCS11_PIN` +`PKCS11_FORCE_LOGIN`, `PKCS11_NO_LOGIN_CACHE`, `PKCS11_PIN` ## Testing the provider operation @@ -208,6 +208,12 @@ The provider supports the following controls: * **debug_level**: Sets the debug level: 0=emerg, 1=alert, 2=crit, 3=err, 4=warning, 5=notice (default), 6=info, 7=debug * **force_login**: Forces login to the PKCS#11 module +* **no_login_cache**: Log out and close the token session as soon as a key is + freed, instead of keeping the login session cached until exit. Opt-in + workaround for tokens with a hard limit on concurrent sessions (e.g. YubiHSM 2, + 16 sessions) driven by many short-lived processes, where exit-time cleanup is + skipped. Re-login happens automatically on the next key load. Leave it off for + long-lived processes that sign repeatedly (they benefit from the cached login). * **init_args**: Specifies additional initialization arguments to the PKCS#11 module diff --git a/src/libp11-int.h b/src/libp11-int.h index f9cee6e1..5f5f50a3 100644 --- a/src/libp11-int.h +++ b/src/libp11-int.h @@ -66,6 +66,7 @@ struct pkcs11_ctx_private { unsigned int forkid; int initialized; void (*vlog_a)(int, const char *, va_list); /* for the logging callback */ + int no_login_cache; /* opt-in: drop the login session on key free */ }; struct pkcs11_keys { @@ -79,6 +80,7 @@ struct pkcs11_slot_private { pthread_mutex_t lock; pthread_cond_t cond; int8_t rw_mode, logged_in; + int8_t no_login_cache; /* copied from ctx at slot creation */ CK_SLOT_ID id; CK_SESSION_HANDLE *session_pool; unsigned int session_head, session_tail, session_poolsize; @@ -262,6 +264,12 @@ extern int pkcs11_login(PKCS11_SLOT_private *, int so, const char *pin); /* De-authenticate from the card */ extern int pkcs11_logout(PKCS11_SLOT_private *); +/* Release the login session without wiping the key cache (opt-in no_login_cache) */ +extern int pkcs11_slot_logout_session_only(PKCS11_SLOT_private *); + +/* Release the login session for the slot backing an EVP_PKEY (provider key-free path) */ +extern void pkcs11_release_login_for_pkey(EVP_PKEY *pkey); + /* Authenticate a private the key operation if needed */ int pkcs11_authenticate(PKCS11_OBJECT_private *key, CK_SESSION_HANDLE session); @@ -315,6 +323,9 @@ extern int pkcs11_remove_object(PKCS11_OBJECT_private *object); extern int pkcs11_set_ui_method(PKCS11_CTX_private *ctx, UI_METHOD *ui_method, void *ui_user_data); +/* Enable/disable dropping the login session when a key is freed */ +extern int pkcs11_set_no_login_cache(PKCS11_CTX_private *ctx, int enable); + /* Initialize a token */ extern int pkcs11_init_token(PKCS11_SLOT_private *, const char *pin, const char *label); diff --git a/src/libp11.exports b/src/libp11.exports index 75ef61ca..1d2e86ee 100644 --- a/src/libp11.exports +++ b/src/libp11.exports @@ -53,6 +53,7 @@ PKCS11_get_ecdh_method PKCS11_pkey_meths ERR_load_PKCS11_strings PKCS11_set_ui_method +PKCS11_set_no_login_cache ERR_get_CKR_code PKCS11_set_vlog_a_method PKCS11_keygen diff --git a/src/libp11.h b/src/libp11.h index e9c9df01..1eeff299 100644 --- a/src/libp11.h +++ b/src/libp11.h @@ -384,6 +384,22 @@ extern int PKCS11_remove_certificate(PKCS11_CERT *); extern int PKCS11_set_ui_method(PKCS11_CTX *ctx, UI_METHOD *ui_method, void *ui_user_data); +/** + * Enable or disable dropping the token login session when a key is freed. + * + * When enabled, the login session is logged out and closed as soon as a private + * key object is freed, instead of being cached until the context is destroyed. + * This is an opt-in workaround for tokens with a hard limit on concurrent + * sessions (e.g. YubiHSM 2) driven by many short-lived processes, where + * exit-time cleanup is skipped. Re-login happens automatically on the next key + * load. Leave it disabled for long-lived processes that sign repeatedly. + * + * @param ctx context allocated by PKCS11_CTX_new() + * @param enable non-zero to drop the login session on key free, 0 to cache it + * @return 0 on success, -1 on error + */ +extern int PKCS11_set_no_login_cache(PKCS11_CTX *ctx, int enable); + /** * Initialize a token * diff --git a/src/p11_ec.c b/src/p11_ec.c index 50d18ba2..e38f10a3 100644 --- a/src/p11_ec.c +++ b/src/p11_ec.c @@ -423,7 +423,14 @@ static void pkcs11_ec_finish(EC_KEY *ec) key = pkcs11_get_ex_data_ec(ec); if (key) { + PKCS11_SLOT_private *slot = key->slot; + pkcs11_set_ex_data_ec(ec, NULL); + /* opt-in: release the login session now, during healthy runtime, + * instead of leaking it because cleanup is skipped at exit */ + if (slot && slot->no_login_cache + && key->object_class == CKO_PRIVATE_KEY) + pkcs11_slot_logout_session_only(slot); pkcs11_object_free(key); } if (ossl_ec_finish) diff --git a/src/p11_front.c b/src/p11_front.c index e8f19f5d..e411e91a 100644 --- a/src/p11_front.c +++ b/src/p11_front.c @@ -409,6 +409,14 @@ int PKCS11_set_ui_method(PKCS11_CTX *pctx, UI_METHOD *ui_method, void *ui_user_d return pkcs11_set_ui_method(ctx, ui_method, ui_user_data); } +int PKCS11_set_no_login_cache(PKCS11_CTX *pctx, int enable) +{ + PKCS11_CTX_private *ctx = pctx->_private; + if (check_fork(ctx) < 0) + return -1; + return pkcs11_set_no_login_cache(ctx, enable); +} + /* External interface to the deprecated features */ int PKCS11_keygen(PKCS11_TOKEN *token, PKCS11_KGEN_ATTRS *kg) diff --git a/src/p11_key.c b/src/p11_key.c index 29d5f216..f8d18844 100644 --- a/src/p11_key.c +++ b/src/p11_key.c @@ -327,6 +327,40 @@ void pkcs11_object_free(PKCS11_OBJECT_private *obj) OPENSSL_free(obj); } +/* + * Release the cached login session for the slot backing this EVP_PKEY, if the + * no_login_cache option is enabled for that slot. Used by the OpenSSL 3 provider + * key-free path (keymgmt_free), where the legacy RSA/EC "finish" callbacks do not + * run because keymgmt_load transfers ownership of the wrapped key to OpenSSL. + */ +void pkcs11_release_login_for_pkey(EVP_PKEY *pkey) +{ + PKCS11_OBJECT_private *obj = NULL; + + if (pkey == NULL) + return; + switch (EVP_PKEY_base_id(pkey)) { + case EVP_PKEY_RSA: { + const RSA *rsa = EVP_PKEY_get0_RSA(pkey); + if (rsa) + obj = pkcs11_get_ex_data_rsa(rsa); + break; + } +#ifndef OPENSSL_NO_EC + case EVP_PKEY_EC: { + const EC_KEY *ec = EVP_PKEY_get0_EC_KEY(pkey); + if (ec) + obj = pkcs11_get_ex_data_ec(ec); + break; + } +#endif + default: + break; + } + if (obj != NULL && obj->slot != NULL && obj->slot->no_login_cache) + pkcs11_slot_logout_session_only(obj->slot); +} + /* Set UI method to allow retrieving CKU_CONTEXT_SPECIFIC PINs interactively */ int pkcs11_set_ui_method(PKCS11_CTX_private *ctx, UI_METHOD *ui_method, void *ui_user_data) diff --git a/src/p11_rsa.c b/src/p11_rsa.c index 8d764864..2c5f2749 100644 --- a/src/p11_rsa.c +++ b/src/p11_rsa.c @@ -430,7 +430,14 @@ static int pkcs11_rsa_free_method(RSA *rsa) RSA_meth_get_finish(RSA_get_default_method()); if (key) { + PKCS11_SLOT_private *slot = key->slot; + pkcs11_set_ex_data_rsa(rsa, NULL); + /* opt-in: release the login session now, during healthy runtime, + * instead of leaking it because cleanup is skipped at exit */ + if (slot && slot->no_login_cache + && key->object_class == CKO_PRIVATE_KEY) + pkcs11_slot_logout_session_only(slot); pkcs11_object_free(key); } if (orig_rsa_free_method) { diff --git a/src/p11_slot.c b/src/p11_slot.c index 56a5c4ad..5333796a 100644 --- a/src/p11_slot.c +++ b/src/p11_slot.c @@ -294,6 +294,66 @@ int pkcs11_logout(PKCS11_SLOT_private *slot) return 0; } +/* + * Release the authenticated login session WITHOUT touching the key cache. + * + * Used by the opt-in no_login_cache behavior: it is called from the RSA/EC + * key "finish" callbacks (i.e. from inside EVP_PKEY_free), so it must NOT call + * pkcs11_logout()/pkcs11_wipe_cache() (those free cached keys via EVP_PKEY_free + * and would re-enter the finish callback -> double free) and must NOT call + * pkcs11_get_session() (which can itself wipe the cache while holding the slot + * lock). It borrows a pooled session handle directly, issues C_Logout (which on + * tokens such as the YubiHSM is what actually releases the authenticated + * session -- C_CloseSession alone does not), then closes and resets the pool. + * + * Idempotent: a lockless logged_in check makes repeated/re-entrant calls a + * no-op, so it never recurses and never nests slot->lock. + */ +int pkcs11_slot_logout_session_only(PKCS11_SLOT_private *slot) +{ + PKCS11_CTX_private *ctx = slot->ctx; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + + if (slot->logged_in < 0) /* not logged in: nothing to do, no lock taken */ + return 0; + + pthread_mutex_lock(&slot->lock); + if (slot->logged_in < 0) { /* lost a race with another logout */ + pthread_mutex_unlock(&slot->lock); + return 0; + } + slot->logged_in = -1; + if (slot->session_head != slot->session_tail) { + /* borrow a pooled session handle to log out on */ + session = slot->session_pool[slot->session_head]; + slot->session_head = (slot->session_head + 1) % slot->session_poolsize; + } + pthread_mutex_unlock(&slot->lock); + + /* C_Logout releases the authenticated session on the token. Best effort. */ + if (session != CK_INVALID_HANDLE) + CRYPTOKI_call(ctx, C_Logout(session)); + + pthread_mutex_lock(&slot->lock); + CRYPTOKI_call(ctx, C_CloseAllSessions(slot->id)); + slot->num_sessions = 0; + slot->session_head = slot->session_tail = 0; + pthread_mutex_unlock(&slot->lock); + + return 0; +} + +/* + * Enable/disable dropping the login session when a key is freed + */ +int pkcs11_set_no_login_cache(PKCS11_CTX_private *ctx, int enable) +{ + if (!ctx) + return -1; + ctx->no_login_cache = enable ? 1 : 0; + return 0; +} + /* * Initialize the token */ @@ -444,6 +504,7 @@ static PKCS11_SLOT_private *pkcs11_slot_new(PKCS11_CTX_private *ctx, CK_SLOT_ID slot->id = id; slot->forkid = ctx->forkid; slot->logged_in = -1; + slot->no_login_cache = (int8_t)ctx->no_login_cache; slot->rw_mode = -1; slot->max_sessions = 16; slot->session_poolsize = slot->max_sessions + 1; diff --git a/src/provider.c b/src/provider.c index 4aa136dc..75b4c45b 100644 --- a/src/provider.c +++ b/src/provider.c @@ -423,6 +423,9 @@ static void *keymgmt_load(const void *reference, size_t reference_sz) /* Free key management object and release associated resources. */ static void keymgmt_free(void *provkey) { + /* opt-in: release the login session during healthy runtime, since the legacy + * RSA/EC finish callbacks never run on the provider path */ + p11_keydata_release_login(provkey); p11_keydata_free(provkey); } diff --git a/src/provider_helpers.c b/src/provider_helpers.c index e7e764e9..5c0affb6 100644 --- a/src/provider_helpers.c +++ b/src/provider_helpers.c @@ -24,6 +24,7 @@ */ #include "provider_helpers.h" +#include "libp11-int.h" /* pkcs11_release_login_for_pkey() */ #include /* isdigit() */ #if defined(__GNUC__) || defined(__clang__) @@ -45,6 +46,7 @@ typedef struct { char *pin; char *debug_level; char *force_login; + char *no_login_cache; char *init_args; } PROVIDER_PARAMS; @@ -65,8 +67,10 @@ struct provider_ctx { char *pin; int debug_level; int force_login; + int no_login_cache; char *p_debug_level; char *p_force_login; + char *p_no_login_cache; /* function offered by libcrypto to the provider */ OSSL_FUNC_core_get_params_fn *core_get_params; @@ -240,6 +244,7 @@ void PROVIDER_CTX_destroy(PROVIDER_CTX *prov_ctx) OPENSSL_free(prov_ctx->pin); OPENSSL_free(prov_ctx->p_debug_level); OPENSSL_free(prov_ctx->p_force_login); + OPENSSL_free(prov_ctx->p_no_login_cache); OPENSSL_free(prov_ctx->init_args); OPENSSL_free(prov_ctx); } @@ -283,6 +288,7 @@ int PROVIDER_CTX_get_core_parameters(PROVIDER_CTX *prov_ctx) {"pin", OSSL_PARAM_UTF8_PTR, &prov_ctx->params.pin, 0, 0}, {"debug_level", OSSL_PARAM_UTF8_PTR, &prov_ctx->params.debug_level, 0, 0}, {"force_login", OSSL_PARAM_UTF8_PTR, &prov_ctx->params.force_login, 0, 0}, + {"no_login_cache", OSSL_PARAM_UTF8_PTR, &prov_ctx->params.no_login_cache, 0, 0}, {"init_args", OSSL_PARAM_UTF8_PTR, &prov_ctx->params.init_args, 0, 0}, OSSL_PARAM_END }; @@ -324,6 +330,9 @@ int PROVIDER_CTX_get_core_parameters(PROVIDER_CTX *prov_ctx) if (prov_ctx->params.force_login) { prov_ctx->p_force_login = OPENSSL_strdup(prov_ctx->params.force_login); } + if (prov_ctx->params.no_login_cache) { + prov_ctx->p_no_login_cache = OPENSSL_strdup(prov_ctx->params.no_login_cache); + } if (prov_ctx->params.init_args) { prov_ctx->init_args = OPENSSL_strdup(prov_ctx->params.init_args); } @@ -383,6 +392,17 @@ int PROVIDER_CTX_set_parameters(PROVIDER_CTX *prov_ctx) if (prov_ctx->force_login) { UTIL_CTX_set_force_login(prov_ctx->util_ctx, 1); } + if (prov_ctx->p_no_login_cache && *prov_ctx->p_no_login_cache != '\0') { + if (isdigit(*prov_ctx->p_no_login_cache)) { + prov_ctx->no_login_cache = (atoi(prov_ctx->p_no_login_cache) != 0); + } else { + prov_ctx->no_login_cache = (strcasecmp("true", prov_ctx->p_no_login_cache) == 0 + || strcasecmp("yes", prov_ctx->p_no_login_cache) == 0); + } + } + if (prov_ctx->no_login_cache) { + UTIL_CTX_set_no_login_cache(prov_ctx->util_ctx, 1); + } return 1; } @@ -512,6 +532,21 @@ void p11_keydata_free(P11_KEYDATA *keydata) OPENSSL_free(keydata); } +/* + * Opt-in (no_login_cache): release the token login session for a private key as + * soon as the provider frees it (keymgmt_free), during healthy runtime, instead + * of leaking it because exit-time cleanup is skipped. Best effort; no-op when the + * option is off or the key is public. + */ +void p11_keydata_release_login(P11_KEYDATA *keydata) +{ + if (keydata == NULL || !keydata->is_private || keydata->pkey == NULL) + return; + if (keydata->prov_ctx == NULL || !keydata->prov_ctx->no_login_cache) + return; + pkcs11_release_login_for_pkey(keydata->pkey); +} + /* Create keydata object from EVP_PKEY and initialize key metadata. */ P11_KEYDATA *p11_keydata_from_evp_pkey(PROVIDER_CTX *ctx, EVP_PKEY *pkey, int is_private) { @@ -1662,6 +1697,11 @@ static void PROVIDER_CTX_get_environment_parameters(PROVIDER_CTX *prov_ctx) OPENSSL_free(prov_ctx->p_force_login); prov_ctx->p_force_login = OPENSSL_strdup(str); } + str = getenv("PKCS11_NO_LOGIN_CACHE"); + if (str != NULL && str[0] != '\0') { + OPENSSL_free(prov_ctx->p_no_login_cache); + prov_ctx->p_no_login_cache = OPENSSL_strdup(str); + } } /* @@ -1678,6 +1718,7 @@ static int PROVIDER_CTX_get_specific_parameters(PROVIDER_CTX *prov_ctx) {"pin", OSSL_PARAM_UTF8_PTR, ¶ms.pin, 0, 0}, {"debug_level", OSSL_PARAM_UTF8_PTR, ¶ms.debug_level, 0, 0}, {"force_login", OSSL_PARAM_UTF8_PTR, ¶ms.force_login, 0, 0}, + {"no_login_cache", OSSL_PARAM_UTF8_PTR, ¶ms.no_login_cache, 0, 0}, {"init_args", OSSL_PARAM_UTF8_PTR, ¶ms.init_args, 0, 0}, OSSL_PARAM_END }; @@ -1712,6 +1753,11 @@ static int PROVIDER_CTX_get_specific_parameters(PROVIDER_CTX *prov_ctx) OPENSSL_free(prov_ctx->p_force_login); prov_ctx->p_force_login = OPENSSL_strdup(params.force_login); } + if (params.no_login_cache && (!prov_ctx->params.no_login_cache + || strcmp(params.no_login_cache, prov_ctx->params.no_login_cache))) { + OPENSSL_free(prov_ctx->p_no_login_cache); + prov_ctx->p_no_login_cache = OPENSSL_strdup(params.no_login_cache); + } if (params.init_args && (!prov_ctx->params.init_args || strcmp(params.init_args, prov_ctx->params.init_args))) { OPENSSL_free(prov_ctx->init_args); diff --git a/src/provider_helpers.h b/src/provider_helpers.h index 1857cee6..25fffe76 100644 --- a/src/provider_helpers.h +++ b/src/provider_helpers.h @@ -79,6 +79,7 @@ int PROVIDER_CTX_set_ui_method(PROVIDER_CTX *prov_ctx, UI_METHOD *ui_method, voi P11_KEYDATA *p11_keydata_new(PROVIDER_CTX *ctx); int p11_keydata_up_ref(P11_KEYDATA *keydata); void p11_keydata_free(P11_KEYDATA *keydata); +void p11_keydata_release_login(P11_KEYDATA *keydata); P11_KEYDATA *p11_keydata_from_evp_pkey(PROVIDER_CTX *ctx, EVP_PKEY *pkey, int is_private); const char *p11_keydata_get_name(P11_KEYDATA *keydata); int p11_keydata_is_private(const P11_KEYDATA *keydata); diff --git a/src/util.h b/src/util.h index a1379e72..334635d6 100644 --- a/src/util.h +++ b/src/util.h @@ -71,6 +71,7 @@ void UTIL_CTX_log(UTIL_CTX *ctx, int level, const char *format, ...) int UTIL_CTX_set_pin(UTIL_CTX *ctx, const char *pin); void UTIL_CTX_set_force_login(UTIL_CTX *ctx, int force_login); +void UTIL_CTX_set_no_login_cache(UTIL_CTX *ctx, int no_login_cache); X509 *UTIL_CTX_get_cert_from_uri(UTIL_CTX *ctx, const char *uri, UI_METHOD *ui_method, void *ui_data); diff --git a/src/util_uri.c b/src/util_uri.c index 2bfc5198..3e558813 100644 --- a/src/util_uri.c +++ b/src/util_uri.c @@ -67,6 +67,7 @@ struct util_ctx_st { size_t pin_length; int forced_pin; int force_login; + int no_login_cache; /* Current operations */ PKCS11_CTX *pkcs11_ctx; @@ -173,6 +174,8 @@ static int util_ctx_init_libp11(UTIL_CTX *ctx) PKCS11_set_vlog_a_method(ctx->pkcs11_ctx, ctx->vlog); PKCS11_CTX_init_args(ctx->pkcs11_ctx, ctx->init_args); PKCS11_set_ui_method(ctx->pkcs11_ctx, ctx->ui_method, ctx->ui_data); + if (ctx->no_login_cache) + PKCS11_set_no_login_cache(ctx->pkcs11_ctx, 1); if (PKCS11_CTX_load(ctx->pkcs11_ctx, ctx->module) < 0) { UTIL_CTX_log(ctx, LOG_ERR, "Unable to load module %s\n", ctx->module); UTIL_CTX_free_libp11(ctx); @@ -470,6 +473,11 @@ void UTIL_CTX_set_force_login(UTIL_CTX *ctx, int force_login) ctx->force_login = force_login; } +void UTIL_CTX_set_no_login_cache(UTIL_CTX *ctx, int no_login_cache) +{ + ctx->no_login_cache = no_login_cache; +} + /* Return 1 if the user has already logged in */ static int slot_logged_in(UTIL_CTX *ctx, PKCS11_SLOT *slot) { int logged_in = 0;