From 500d370ab865947d6e6de9a273f3a42eb7c82205 Mon Sep 17 00:00:00 2001 From: Artemii Bigdan Date: Sun, 21 Jun 2026 08:41:54 +0200 Subject: [PATCH gnupg] scd: Add Nitrokey 3 for pcsc-shared mode Nitrokey 3 (NK3) is a multi-application token with both OpenPGP and PIV applets, like Yubikey. When scdaemon uses pcsc-shared, external applications (e.g. KeePassXC) can disturb the card state and deselect the OpenPGP applet, causing "Card error" on subsequent operations. Commit rG044e5a3c3801 (T5484) added check_external_interference() to detect this condition, but limited it to CARDTYPE_YUBIKEY. Extend the detection to also cover CARDTYPE_NK3. Additionally, the ATR-based card type heuristics in app_new_register() is moved to run unconditionally, not only when SELECT MF (0x3F00) succeeds. Some cards (including NK3) do not support SELECT MF in all states, which previously routed them through the Yubikey detection path, skipping ATR detection and leaving the cardtype as CARDTYPE_GENERIC. Unlike D535 which proposed a generic fix for all cards under pcsc-shared, this patch takes a more conservative approach: add NK3 alongside Yubikey everywhere, preserving all existing behavior for other card types. Based on initial work by Arnaud Patard . GnuPG-bug-id: T5484 Signed-off-by: Artemii Bigdan --- scd/app-common.h | 1 + scd/app-openpgp.c | 3 ++- scd/app-piv.c | 5 ++++- scd/app.c | 46 ++++++++++++++++++++++++++++++++-------------- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/scd/app-common.h b/scd/app-common.h index 03b6c4884..137bda196 100644 --- a/scd/app-common.h +++ b/scd/app-common.h @@ -55,6 +55,7 @@ typedef enum { CARDTYPE_GENERIC = 0, CARDTYPE_GNUK, + CARDTYPE_NK3, CARDTYPE_YUBIKEY, CARDTYPE_ZEITCONTROL, CARDTYPE_SCE7 /* G+D SmartCafe Expert 7.0 */ diff --git a/scd/app-openpgp.c b/scd/app-openpgp.c index b2467d05f..c19ba308a 100644 --- a/scd/app-openpgp.c +++ b/scd/app-openpgp.c @@ -6502,7 +6502,8 @@ do_reselect (app_t app, ctrl_t ctrl) /* An extra check which should not be necessary because the caller * should have made sure that a re-select is only called for * appropriate cards. */ - if (APP_CARD(app)->cardtype != CARDTYPE_YUBIKEY) + if (APP_CARD(app)->cardtype != CARDTYPE_YUBIKEY + && APP_CARD(app)->cardtype != CARDTYPE_NK3) return gpg_error (GPG_ERR_NOT_SUPPORTED); /* Note that the card can't cope with P2=0xCO, thus we need to pass diff --git a/scd/app-piv.c b/scd/app-piv.c index dc92bd2e2..280816b29 100644 --- a/scd/app-piv.c +++ b/scd/app-piv.c @@ -211,6 +211,7 @@ struct app_local_s { struct { unsigned int yubikey:1; /* This is on a Yubikey. */ + unsigned int nk3:1; /* This is on a Nitrokey 3. */ } flags; /* Keep track on whether we cache a certain PIN so that we get it @@ -3607,7 +3608,7 @@ do_reselect (app_t app, ctrl_t ctrl) /* An extra check which should not be necessary because the caller * should have made sure that a re-select is only called for * appropriate cards. */ - if (!app->app_local->flags.yubikey) + if (!app->app_local->flags.yubikey && !app->app_local->flags.nk3) return gpg_error (GPG_ERR_NOT_SUPPORTED); err = iso7816_select_application (app_get_slot (app), @@ -3727,6 +3728,8 @@ app_select_piv (app_t app) if (app->card->cardtype == CARDTYPE_YUBIKEY) app->app_local->flags.yubikey = 1; + if (app->card->cardtype == CARDTYPE_NK3) + app->app_local->flags.nk3 = 1; /* If we don't have a s/n construct it from the CHUID. */ if (!APP_CARD(app)->serialno) diff --git a/scd/app.c b/scd/app.c index 4f4601d05..85f434c62 100644 --- a/scd/app.c +++ b/scd/app.c @@ -123,6 +123,7 @@ strcardtype (cardtype_t t) { case CARDTYPE_GENERIC: return "generic"; case CARDTYPE_GNUK: return "gnuk"; + case CARDTYPE_NK3: return "nitrokey3"; case CARDTYPE_YUBIKEY: return "yubikey"; case CARDTYPE_ZEITCONTROL: return "zeitcontrol"; case CARDTYPE_SCE7: return "smartcafe"; @@ -597,7 +598,8 @@ check_application_conflict (card_t card, const char *name, if (card->app->apptype == APPTYPE_UNDEFINED) return 0; - if (card->cardtype == CARDTYPE_YUBIKEY) + if (card->cardtype == CARDTYPE_YUBIKEY + || card->cardtype == CARDTYPE_NK3) { if (card->app->apptype == APPTYPE_OPENPGP) { @@ -799,27 +801,34 @@ app_new_register (int slot, ctrl_t ctrl, const char *name, } xfree (buf); } - else - card->cardtype = atr_to_cardtype (slot, NULL, 0); } - else /* Got 3F00 */ + + /* Use ATR-based heuristics to identify card types. This runs + * regardless of whether SELECT MF succeeded, because some cards + * (e.g. Nitrokey 3) do not support SELECT MF and would otherwise + * fall through to the non-Yubikey path without ATR detection. */ + if (card->cardtype == CARDTYPE_GENERIC) { unsigned char *atr; size_t atrlen; - /* This is heuristics to identify different implementations. */ - /* FIXME: The first two checks are pretty OpenPGP card specific. */ atr = apdu_get_atr (slot, &atrlen); if (atr) { + /* FIXME: The first two checks are pretty OpenPGP card + * specific. */ if (atrlen == 21 && atr[2] == 0x11) card->cardtype = CARDTYPE_GNUK; else if (atrlen == 21 && atr[7] == 0x75) card->cardtype = CARDTYPE_ZEITCONTROL; + else if (atrlen >= 16 && !memcmp (&atr[5], "Nitrokey", 8)) + card->cardtype = CARDTYPE_NK3; else card->cardtype = atr_to_cardtype (slot, atr, atrlen); xfree (atr); } + else + card->cardtype = atr_to_cardtype (slot, NULL, 0); } if (!err && card->cardtype != CARDTYPE_YUBIKEY) @@ -1171,7 +1180,8 @@ select_all_additional_applications_internal (ctrl_t ctrl, card_t card) int i, j; int any_new = 0; - if (card->cardtype == CARDTYPE_YUBIKEY) + if (card->cardtype == CARDTYPE_YUBIKEY + || card->cardtype == CARDTYPE_NK3) { candidates[0] = APPTYPE_OPENPGP; candidates[1] = APPTYPE_PIV; @@ -1745,14 +1755,17 @@ check_external_interference (app_t app, ctrl_t ctrl) { /* * Only when a user is using Yubikey with pcsc-shared configuration, - * we need this detection. Otherwise, the card/token is under full - * control of scdaemon, there's no problem at all. However, if the - * APDU command has been used we better also check whether the AID + * or a Nitrokey 3 with pcsc-shared configuration, we need this + * detection. Otherwise, the card/token is under full control of + * scdaemon, there's no problem at all. However, if the APDU + * command has been used we better also check whether the AID * is still valid. */ if (app && app->card && app->card->maybe_check_aid) app->card->maybe_check_aid = 0; - else if (!opt.pcsc_shared || app->card->cardtype != CARDTYPE_YUBIKEY) + else if (!opt.pcsc_shared + || (app->card->cardtype != CARDTYPE_YUBIKEY + && app->card->cardtype != CARDTYPE_NK3)) return 0; if (app->fnc.check_aid) @@ -1791,12 +1804,17 @@ maybe_switch_app (ctrl_t ctrl, card_t card, const char *keyref) if (!card->app) return gpg_error (GPG_ERR_CARD_NOT_INITIALIZED); - if (card->maybe_check_aid && card->app->fnc.reselect + if ((card->maybe_check_aid + || (opt.pcsc_shared + && (card->cardtype == CARDTYPE_YUBIKEY + || card->cardtype == CARDTYPE_NK3))) + && card->app->fnc.reselect && check_external_interference (card->app, ctrl)) { if (DBG_APP) - log_debug ("slot %d, app %s: forced re-select due to direct APDU use\n", - card->slot, xstrapptype (card->app)); + log_debug ("slot %d, app %s: forced re-select due to %s\n", + card->slot, xstrapptype (card->app), + card->maybe_check_aid? "direct APDU use" : "pcsc-shared"); err = card->app->fnc.reselect (card->app, ctrl); if (err) log_error ("slot %d, app %s: forced re-select failed: %s - ignored\n", -- 2.54.0