Skip to content

Conversation

@xhanulik
Copy link
Member

@xhanulik xhanulik commented Apr 5, 2023

In C_GetSessionInfo, the existing session should be removed when the card or reader was removed/has changed and not only by logging out of the card.

When checking the functionality of #2666, it was found that IDPrime 940 works with two PINs - default PIN and signature PIN. For signature PIN, it holds that we must log into the card before every operation with a private key, and the card then logs out after every such operation. The pkcs11-tool test revealed that this behaviour might cause problems when after such a sensitive operation C_GetSessionInfo() is called since it checks whether the card is in the logged-in state, and if not, it removes all existing sessions.

Removing of sessions in C_GetSessionInfo was introduced by #1875.
Changes were tested with IDPrime 940 (rebased with changes from #2666) and Yubikey 5.

Checklist
  • PKCS#11 module is tested
  • Windows minidriver is tested
  • macOS tokend is tested

@dengert
Copy link
Member

dengert commented Apr 5, 2023

For signature PIN, it holds that we must log into the card before every operation with a private key, and the card then logs out after every such operation.

This sounds very similar to PIV and which requires the pin (same one as login) to be entered before using the signature key. (The card enforces this - the crypt apdu must be preceded by the pin verify apdu.)

You might be able to use the feature:

  • PKCS11 and pkcs11-tool supports the CKA_ALWAYS_AUTHENTICATE attribute of the key and the use of C_Login with CKU_CONTEXT_SPECIFIC to prompt the user for every use of this key.
  • PKCS15 refers this as user_consent in sc_pkcs15_object
  • Also see SC_AC_CONTEXT_SPECIFIC for pin operations.
  • PIN caching will not use the cached pin for C_Login with CKU_CONTEXT_SPECIFIC (Can be overridden with pin_cache_ignore_user_consent. Might need some work if two pins are not the same.)

@frankmorgner
Copy link
Member

Currently we are only doing presence detection in C_WaitForSlotEvent, C_Initalize, C_WaitForSlotEvent, C_GetSlotInfo (via card_detect). We don't do it in any of the session commands or before any of the token operations. I wonder what problems you are anticipating that may need the detection in C_GetSessionInfo...

@Jakuje
Copy link
Member

Jakuje commented Apr 13, 2023

I wonder what problems you are anticipating that may need the detection in C_GetSessionInfo...

The GetSessionInfo is usually the first call the application does. Either straight away before login, after login or after some idle period, to verify the session is still usable before trying to do some operation on the module. The original problem and problem solved here is the long-running login session, used by various client applications, including OpenSSH's ssh-agent, but probably also Firefox/NSS (but these have another tread monitoring the card with C_WaitForSlotEvent), but probably others.

For signature PIN, it holds that we must log into the card before every operation with a private key, and the card then logs out after every such operation.

This sounds very similar to PIV and which requires the pin (same one as login) to be entered before using the signature key. (The card enforces this - the crypt apdu must be preceded by the pin verify apdu.)

In case of PIV, I think the signature PIN is always the same as the login pin, therefore the PIV presents all the objects in one slot and enforces the "always authenticate" behavior (and checks the login status through a discovery object). The IDPrime aligns more with the pkcs15 definition, where different keys are accessible with different PINs, which in PKCS#11 maps different PINs to different slots, surfacing this problem (after one signature operation, the login status is lost which is causing invalidation of all sessions, which is obviously wrong).

The summary is that this is mostly attempt to revisit changes made in 4bd8cda which were incomplete and were looking into the login statues instead of to the card/reader status. The fixups 8ab39bd and c2e00e9 were trying to fix it for various other use cases, but they still kept looking into the login status instead of checking the real trigger for resetting all sessions -- reader status.

if (priv->reader_state.dwEventState & SCARD_STATE_PRESENT && !(reader->flags & SC_READER_CARD_PRESENT))
reader->flags |= SC_READER_CARD_CHANGED;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this part deserves a comment in the code why it is needed to be done this way, probably updating the above comment.
Also I am not sure if this can have any side effects. As far as I remember, the timeout from SCardGetStatusChange() returns when there was no change made to the reader since the last call. So this condition roughly to the case when the card is now present, but we believe previously it was not present (which sounds like the SCardGetStatusChange() was called in between the reader was removed and inserted, but I would have to try myself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I will update the comment.
Regarding the SCardGetStatusChange(), when no change happens, the SCARD_E_TIMEOUT is returned. Then, when reader is removed, next call returns SCARD_E_UNKNOWN_READER and SC_READER_CARD_PRESENT is removed from flags (but no SC_READER_CARD_CHANGED is set though). When reader is then inserted back again, the return code is again SCARD_E_TIMEOUT.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above (and the change to refresh_attributes) is somewhat confusing. If a reader is removed, then it should be treated as removed. If it is inserted again, it should be treated like a new reader, because all card state is lost and additionally you can't be sure whether this is really the very same card that's inserted.

For me this seems like SCARD_E_UNKNOWN_READER is not treated correctly. This change may help (note that the first change actually fixes a check to SC_READER_REMOVED as well):

diff --git a/src/libopensc/reader-pcsc.c b/src/libopensc/reader-pcsc.c
index 04d5ac8f..8c2f98c9 100644
--- a/src/libopensc/reader-pcsc.c
+++ b/src/libopensc/reader-pcsc.c
@@ -364,7 +364,7 @@ static int refresh_attributes(sc_reader_t *reader)
        if (reader->ctx->flags & SC_CTX_FLAG_TERMINATE)
                return SC_ERROR_NOT_ALLOWED;
 
-       if (priv->reader_state.szReader == NULL || reader->ctx->flags & SC_READER_REMOVED) {
+       if (priv->reader_state.szReader == NULL || reader->flags & SC_READER_REMOVED) {
                priv->reader_state.szReader = reader->name;
                priv->reader_state.dwCurrentState = SCARD_STATE_UNAWARE;
                priv->reader_state.dwEventState = SCARD_STATE_UNAWARE;
@@ -394,6 +394,8 @@ static int refresh_attributes(sc_reader_t *reader)
 #endif
                                || rv == (LONG)SCARD_E_SERVICE_STOPPED) {
                        reader->flags &= ~(SC_READER_CARD_PRESENT);
+                       reader->flags |= SC_READER_REMOVED;
+                       priv->gpriv->removed_reader = reader;
                        SC_FUNC_RETURN(reader->ctx, SC_LOG_DEBUG_VERBOSE, SC_SUCCESS);
                }

@Jakuje
Copy link
Member

Jakuje commented May 4, 2023

Sorry for a long post.

Looking into this issue again, I think there might be one more issue that I did not notice before. When applications check for the slot/card presence, they usually use a sequence of C_GetSlotList() and C_GetSlotInfo() (but can be also reproduced with pkcs11-tool --test-hotplug). They call C_GetSlotList() twice -- first just for the size inquiry and second to fill the provided buffer. And both of these calls do the full card detection. This brings me to the question:

  • Should this be changed on the pkcs11 level to cache the results from the first call?

This basically goes through the card detection needlessly twice. It can also be racy as if the slots change in between, we might get unexpected CKR_BUFFER_TOO_SMALL (which might choke some apps). But this would be for separate issue, I will open later.

I tried again to reproduce the issue with #1822 (comment) with this branch, but it fails spectacularly. Most probably related to 7b782d5. After rebasing on current master it prompts for the pin correctly, but after reinsertion, I am asked for the PIN twice:

  • first I am getting the CKR_USER_ALREADY_LOGGED_IN from the C_Login(), which is certainly wrong
  • then the C_Sign fails with CKR_USER_NOT_LOGGED_IN
  • then it again asks for the pin and the connection is established.

I do not see the invalidation of all the sessions as expected right after the reinsertion.

Debug logs from this attempt:

  • the old session is closed:
pkcs11-session.c:109:sc_pkcs11_close_session: real C_CloseSession(0x15680274bf00)
  • we get C_GetSlotInfo() call. This looks like the token is still inserted:
pkcs11-global.c:629:C_GetSlotInfo: C_GetSlotInfo(0x0)
slot.c:393:card_detect_all: Detect all cards
slot.c:219:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Detecting smart card
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:379:refresh_attributes: reader->flags=0x5, priv->reader_state.dwEventState=0x120
reader-pcsc.c:390:refresh_attributes: returning with: 0 (Success)
reader-pcsc.c:481:pcsc_detect_card_presence: returning with: 5
sc.c:351:sc_detect_card_presence: returning with: 5
slot.c:376:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Detection ended
slot.c:432:card_detect_all: All cards detected
pkcs11-global.c:641:C_GetSlotInfo: VSS C_GetSlotInfo found
pkcs11-global.c:642:C_GetSlotInfo: C_GetSlotInfo() get slot rv CKR_OK
slot.c:219:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Detecting smart card
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:379:refresh_attributes: reader->flags=0x5, priv->reader_state.dwEventState=0x120
reader-pcsc.c:390:refresh_attributes: returning with: 0 (Success)
reader-pcsc.c:481:pcsc_detect_card_presence: returning with: 5
sc.c:351:sc_detect_card_presence: returning with: 5
slot.c:376:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Detection ended
pkcs11-global.c:651:C_GetSlotInfo: C_GetSlotInfo() card detect rv 0x0
pkcs11-global.c:668:C_GetSlotInfo: C_GetSlotInfo() flags 0x7
pkcs11-global.c:672:C_GetSlotInfo: C_GetSlotInfo(0x0) = CKR_OK

From another thread it looks like the NSS checks the validity of the key by calling C_GetAttributeValue() on the object used to authentication. Then it checks C_GetSessionInfo() which detects the change:

pkcs11-session.c:265:C_GetSessionInfo: C_GetSessionInfo(hSession:0x156801548540)
pkcs11-session.c:273:C_GetSessionInfo: C_GetSessionInfo(slot:0x0)
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:409:refresh_attributes: current  state: 0x00000022   [present|changed]
reader-pcsc.c:410:refresh_attributes: previous state: 0x00000120   [shared mode|present]
reader-pcsc.c:463:refresh_attributes: card present, changed
reader-pcsc.c:481:pcsc_detect_card_presence: returning with: 3
sc.c:351:sc_detect_card_presence: returning with: 3

this notices the state change, but still present. Then we skip the newly introduced condition in pkcs11-session.c and go to checking PIN info, while we discovered the card was changed and we should have killed the existing sessions instead of trying resurrect them somehow. The pin info kind of worked

pkcs15-pin.c:707:sc_pkcs15_get_pin_info: called
card.c:471:sc_lock: called
reader-pcsc.c:691:pcsc_lock: called
reader-pcsc.c:700:pcsc_lock: Yubico YubiKey OTP+FIDO+CCID 00 00:SCardBeginTransaction returned: 0x80100011
reader-pcsc.c:612:pcsc_connect: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:379:refresh_attributes: reader->flags=3, priv->reader_state.dwEventState=20
reader-pcsc.c:390:refresh_attributes: returning with: 0 (Success)

The NSS calls C_GetSessionInfo() which looks working again. Then we call the C_Login(), which is short-circuited as user-already logged in without any log message:

pkcs11-session.c:352:C_Login: C_Login(0x156801548540, 1)
pkcs11-session.c:374:C_Login: C_Login() slot->login_user 1

Then we go to open session & Sign attempt where init works, but later we find out we are not logged in:

pkcs11-session.c:58:C_OpenSession: C_OpenSession(0x0)
slot.c:472:slot_get_token: Slot(id=0x0): get token
slot.c:490:slot_get_token: Slot-get-token returns OK
pkcs11-session.c:94:C_OpenSession: C_OpenSession handle: 0x1568010e85b0
pkcs11-session.c:97:C_OpenSession: C_OpenSession() = CKR_OK
framework-pkcs15.c:3941:pkcs15_prkey_get_attribute: pkcs15_prkey_get_attribute() called
framework-pkcs15.c:3941:pkcs15_prkey_get_attribute: pkcs15_prkey_get_attribute() called
mechanism.c:377:sc_pkcs11_sign_init: called
mechanism.c:382:sc_pkcs11_sign_init: mechanism 0x1041, key-type 0x3
misc.c:267:session_start_operation: called
misc.c:268:session_start_operation: Session 0x1568010e85b0, type 1
mechanism.c:504:sc_pkcs11_signature_init: called
framework-pkcs15.c:4662:pkcs15_prkey_can_do: called
framework-pkcs15.c:4663:pkcs15_prkey_can_do: check hardware capabilities: CK_MECHANISM_TYPE=0x1041 (CKM) and CKF_xxx=0x800
framework-pkcs15.c:4671:pkcs15_prkey_can_do: returning with: 84
mechanism.c:558:sc_pkcs11_signature_init: returning with: 0 (Success)
mechanism.c:411:sc_pkcs11_sign_init: returning with: 0 (Success)
pkcs11-object.c:697:C_SignInit: C_SignInit() = CKR_OK

Up until now, there was no APDU call that could detect the login state was reset in the yubikey (might be limitation of my old yubikey, the PIV driver), but the change state from the pcsc is ignored, which is the main cause, I think.


During the second attempt, it looks like it behaves as expected (when I waited 5-10 seconds before reinserting the token), which means that this is still racy -- depending on if we will get the C_SessionInfo/C_GetSlotInfo between the token is removed and reinserted.

From the second attempt, I see the following list of events:

  • closing old signature session
pkcs11-session.c:163:C_CloseSession: C_CloseSession(0xd8006f86fb0)
  • calling GetSlotInfo() finds the token absent so it removes the card and closes the sessions:
pkcs11-global.c:629:C_GetSlotInfo: C_GetSlotInfo(0x0)
slot.c:393:card_detect_all: Detect all cards
slot.c:219:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Detecting smart card
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:400:refresh_attributes: returning with: 0 (Success)
reader-pcsc.c:483:pcsc_detect_card_presence: returning with: 0 (Success)
sc.c:351:sc_detect_card_presence: returning with: 0 (Success)
slot.c:228:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: card absent
slot.c:191:card_removed: Yubico YubiKey OTP+FIDO+CCID 00 00: card removed
slot.c:501:slot_token_removed: slot_token_removed(0x0)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x0) 1
pkcs11-session.c:109:sc_pkcs11_close_session: real C_CloseSession(0xd800684fd40)
pkcs15-pin.c:863:sc_pkcs15_pincache_clear: called
framework-pkcs15.c:1753:pkcs15_release_token: pkcs15_release_token() not implemented
slot.c:501:slot_token_removed: slot_token_removed(0x1)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x1) 0
slot.c:501:slot_token_removed: slot_token_removed(0x2)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x2) 0
slot.c:501:slot_token_removed: slot_token_removed(0x3)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x3) 0
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:400:refresh_attributes: returning with: 0 (Success)
reader-pcsc.c:483:pcsc_detect_card_presence: returning with: 0 (Success)
sc.c:351:sc_detect_card_presence: returning with: 0 (Success)
pkcs15.c:1371:sc_pkcs15_unbind: called
pkcs15-pin.c:863:sc_pkcs15_pincache_clear: called
misc.c:71:sc_to_cryptoki_error_common: libopensc return value: 0 (Success)
card.c:414:sc_disconnect_card: called
card-piv.c:3030:piv_finish: called
reader-pcsc.c:676:pcsc_disconnect: Yubico YubiKey OTP+FIDO+CCID 00 00:SCardDisconnect returned: 0x00000000
card.c:434:sc_disconnect_card: returning with: 0 (Success)
slot.c:432:card_detect_all: All cards detected
pkcs11-global.c:641:C_GetSlotInfo: VSS C_GetSlotInfo found
pkcs11-global.c:642:C_GetSlotInfo: C_GetSlotInfo() get slot rv CKR_OK
  • second thread checking the C_GetAttributeValue() gets CKR_SESSION_HANDLE_INVALID which makes it closing the session.
  • It opens a new session and starts detecting the cards again but for some reason again goes into closing sessions (why?). But after that, the card detection looks like starting again and going ok:
pkcs11-session.c:58:C_OpenSession: C_OpenSession(0x0)
slot.c:472:slot_get_token: Slot(id=0x0): get token
slot.c:480:slot_get_token: Slot(id=0x0): get token: now detect card
slot.c:219:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Detecting smart card
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:409:refresh_attributes: current  state: 0x00000022
reader-pcsc.c:410:refresh_attributes: previous state: 0x00000120
reader-pcsc.c:463:refresh_attributes: card present, changed
reader-pcsc.c:481:pcsc_detect_card_presence: returning with: 3
sc.c:351:sc_detect_card_presence: returning with: 3
slot.c:235:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Card changed
slot.c:191:card_removed: Yubico YubiKey OTP+FIDO+CCID 00 00: card removed
slot.c:501:slot_token_removed: slot_token_removed(0x0)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x0) 0
slot.c:501:slot_token_removed: slot_token_removed(0x1)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x1) 0
slot.c:501:slot_token_removed: slot_token_removed(0x2)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x2) 0
slot.c:501:slot_token_removed: slot_token_removed(0x3)
pkcs11-session.c:145:sc_pkcs11_close_all_sessions: real C_CloseAllSessions(0x3) 0
sc.c:340:sc_detect_card_presence: called
reader-pcsc.c:473:pcsc_detect_card_presence: called
reader-pcsc.c:362:refresh_attributes: Yubico YubiKey OTP+FIDO+CCID 00 00 check
reader-pcsc.c:379:refresh_attributes: reader->flags=3, priv->reader_state.dwEventState=20
reader-pcsc.c:390:refresh_attributes: returning with: 0 (Success)
reader-pcsc.c:481:pcsc_detect_card_presence: returning with: 1
sc.c:351:sc_detect_card_presence: returning with: 1
slot.c:256:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: First seen the card•
slot.c:265:card_detect: Yubico YubiKey OTP+FIDO+CCID 00 00: Connecting ...•
card.c:254:sc_connect_card: called

So this looks like the current version still needs some love.

  • when the token is removed for short time and application does not query in between, it does not work
  • when the token is removed for longer time, the application queries the token, finds it removed and invalidates what is needed (I think in the current state more times than it should).

@Jakuje
Copy link
Member

Jakuje commented May 4, 2023

Test server:

#module: opensc-pkcs11.so
module: /home/jjelen/devel/OpenSC/src/pkcs11/.libs/opensc-pkcs11.so
log-calls: yes
enable-in: chromium-browser
  • Modify the opensc.conf for local built (in my case the default path is /usr/local/etc/opensc.conf) to log into separate file:
	debug = 9;
	debug_file = /tmp/opensc-debug.txt;
  • start the browser chromium-browser 2>&1| tee /tmp/pkcs11.log -- the p11-kit will log the pkcs11 calls to that file
  • connect to https://localhost:5556 -- ignore any security warnings
  • it should ask for the pin, for the certificate to use and then show the certificate that was used for "authentication".

if (priv->reader_state.dwEventState & SCARD_STATE_PRESENT && !(reader->flags & SC_READER_CARD_PRESENT))
reader->flags |= SC_READER_CARD_CHANGED;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above (and the change to refresh_attributes) is somewhat confusing. If a reader is removed, then it should be treated as removed. If it is inserted again, it should be treated like a new reader, because all card state is lost and additionally you can't be sure whether this is really the very same card that's inserted.

For me this seems like SCARD_E_UNKNOWN_READER is not treated correctly. This change may help (note that the first change actually fixes a check to SC_READER_REMOVED as well):

diff --git a/src/libopensc/reader-pcsc.c b/src/libopensc/reader-pcsc.c
index 04d5ac8f..8c2f98c9 100644
--- a/src/libopensc/reader-pcsc.c
+++ b/src/libopensc/reader-pcsc.c
@@ -364,7 +364,7 @@ static int refresh_attributes(sc_reader_t *reader)
        if (reader->ctx->flags & SC_CTX_FLAG_TERMINATE)
                return SC_ERROR_NOT_ALLOWED;
 
-       if (priv->reader_state.szReader == NULL || reader->ctx->flags & SC_READER_REMOVED) {
+       if (priv->reader_state.szReader == NULL || reader->flags & SC_READER_REMOVED) {
                priv->reader_state.szReader = reader->name;
                priv->reader_state.dwCurrentState = SCARD_STATE_UNAWARE;
                priv->reader_state.dwEventState = SCARD_STATE_UNAWARE;
@@ -394,6 +394,8 @@ static int refresh_attributes(sc_reader_t *reader)
 #endif
                                || rv == (LONG)SCARD_E_SERVICE_STOPPED) {
                        reader->flags &= ~(SC_READER_CARD_PRESENT);
+                       reader->flags |= SC_READER_REMOVED;
+                       priv->gpriv->removed_reader = reader;
                        SC_FUNC_RETURN(reader->ctx, SC_LOG_DEBUG_VERBOSE, SC_SUCCESS);
                }

@frankmorgner
Copy link
Member

frankmorgner commented May 4, 2023

I still remember that I needed several iterations of #1923, primarily due to the different layers of state keeping (and swallowing). I think, if SCARD_E_UNKNOWN_READER was not swallowed by refresh_attributes, then C_GetSessionInfo would not step over it, which might trigger the wanted removal of the sessions.

Should this be changed on the pkcs11 level to cache the results from the first call?

No, I don't think we need to overengineer this. As you've pointed out, the worst thing that could happen is CKR_BUFFER_TOO_SMALL, which applications should be able to cope with. Since the application does not have any knowledge about the content of the buffer, the impact of a race condition should be very low.

In any case, PC/SC changes need to be tested on macOS and Windows as well.

xhanulik and others added 5 commits May 12, 2023 09:00
By using sc_detect_card_presence() function and observing status flags.
Do not delete all sessions, when the card was logged out during operation.
Instead, check whether the card was changed and by logging out only set
correct flag.
@xhanulik
Copy link
Member Author

I tried the changes introduced in #2740 (comment) regarding the SC_READER_REMOVED flag and they seem to work ok even without the original wild changes to the refresh_attributes, when SCardGetStatusChange returns SCARD_E_TIMEOUT.
I tested this with gnu-tls server (#2740 (comment)) and yubikey, and it looks like that the problem with CKR_USER_ALREADY_LOGGED_IN after quick reinsertion should be fixed.

Copy link
Member

@frankmorgner frankmorgner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the change on the PC/SC level looks simple enough to assume that it will also work on macOS and Windows without in-depth-testing.

Comment on lines 1424 to 1425
out:
return 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goto/label here is not even saving any repeated code as it contains only one return. I would probably propose to simplify this to call return 0 directly in case of errors.

Short doc text explaining at least what is the meaning of the returned values would also help future readers.

@Jakuje
Copy link
Member

Jakuje commented May 16, 2023

It looks like I managed to get the CKR_USER_NOT_LOGGED_IN with the current version yet again. I even tried with exclusive access to the pcsc reader:

	reader_driver pcsc {
		connect_exclusive = true;
	}

Now I waited couple of seconds before the reinsertion, but pulled out and removed between two blinks of the yubikey (which is probably initiated by the periodic check by NSS).

The OpenSC debug log looks like previously -- there is no sign from the pcsc that there was a change on of the reader. So I decided to have a better look if this is something we could detect from the pcsc-spy logs.

The SCardGetStatusChange really returns SCARD_E_TIMEOUT around the reinsertion time (unfortunately, there are no timestamps), but I can be quite sure that this is the time because there are some following lines where we get signature failure with status bytes 63 C3):

        SCardGetStatusChange
         i hContext: 0x0EF02988
         i dwTimeout: 0x00000000 (0)
         i cReaders: 1
         i szReader: Yubico YubiKey OTP+FIDO+CCID 00 00
         i  dwCurrentState: SCARD_STATE_PRESENT (0x00000020)
         i  dwEventState: SCARD_STATE_PRESENT (0x00000020)
         i  Atr length: 0x00000012 (18)
         i  Atr: 3B F8 13 00 00 81 31 FE 15 59 75 62 69 6B 65 79 34 D4
         o szReader: Yubico YubiKey OTP+FIDO+CCID 00 00
         o  dwCurrentState: SCARD_STATE_PRESENT (0x00000020)
         o  dwEventState: SCARD_STATE_PRESENT (0x00000020)
         o  Atr length: 0x00000012 (18)
         o  Atr: 3B F8 13 00 00 81 31 FE 15 59 75 62 69 6B 65 79 34 D4
         => Command timeout. (SCARD_E_TIMEOUT [0x8010000A])  [0.001311]

But from what I did not notice in the OpenSC logs, there is one call where SCardBeginTransaction failed, which is probably the outcome of the reader removal:

        SCardBeginTransaction
         i hCard: 0x7E8AF4B6
         => Invalid value given. (SCARD_E_INVALID_VALUE [0x80100011])  [0.000091]

After this, opensc reconnects automatically:

        SCardConnect
         i hContext: 0x0EF02988
         i szReader Yubico YubiKey OTP+FIDO+CCID 00 00
         i dwShareMode: SCARD_SHARE_EXCLUSIVE (0x00000001)
         i dwPreferredProtocols: 0x00000003 (T=0, T=1)
         i phCard 0x0002505B (151643)
         i pdwActiveProtocol 0x7F2FF42393B0 (139843936162736)
         o phCard 0x4728F331 (1193866033)
         o dwActiveProtocol: T=1 (0x00000002)
         => Command successful. (SCARD_S_SUCCESS [0x00000000])  [0.017392]
        SCardBeginTransaction
         i hCard: 0x4728F331
         => Command successful. (SCARD_S_SUCCESS [0x00000000])  [0.000136]

but still considers the card/reader good and does not invalidate the state, which is something I am not completely sure it is correct. This is handled in pcsc_lock() where we go to the pcsc_connect() and return useful SC_ERROR_READER_REATTACHED. This is handled in sc_lock() function. But leads only to cache invalidation and nothing more.

So my question would be if the following block should handle also invalidation of the whole card structures or if this code path is likely to be hit also in some other use cases:

OpenSC/src/libopensc/card.c

Lines 478 to 484 in 493a3d4

r = card->reader->ops->lock(card->reader);
while (r == SC_ERROR_CARD_RESET || r == SC_ERROR_READER_REATTACHED) {
sc_invalidate_cache(card);
if (was_reset++ > 4) /* TODO retry a few times */
break;
r = card->reader->ops->lock(card->reader);
}

@LudovicRousseau can you have a look? Should SCardGetStatusChange guarantee to report short-time reader removals/reinsertion? Or is using the invalid card handle reports better/useful indication of reader removal/reinsertion?

@LudovicRousseau
Copy link
Member

@Jakuje Yes, SCardGetStatusChange() should guarantee to report short-time reader removals/reinsertion.
But if the reader is removed and reinserted between 2 consecutive SCardGetStatusChange() calls then you will NOT be notified by SCardGetStatusChange() (I just checked on pcsc-lite. I have not checked on Windows).

This is because the same PC/SC reader name is reused. For example I have this reader name 'Gemalto PC Twin Reader 00 00'. If I remove and reinsert the reader I will get the same reader name. So the second call to SCardGetStatusChange() will use the old reader name but that name also corresponds to the "new" reader.

Also if the reader is removed before SCardBeginTransaction() then you get the error SCARD_E_INVALID_VALUE.
I just added this value in the documentation.

@Jakuje
Copy link
Member

Jakuje commented May 17, 2023

But if the reader is removed and reinserted between 2 consecutive SCardGetStatusChange() calls then you will NOT be notified by SCardGetStatusChange() (I just checked on pcsc-lite. I have not checked on Windows).

Thanks for checking!

If I remove and reinsert the reader I will get the same reader name. So the second call to SCardGetStatusChange() will use the old reader name but that name also corresponds to the "new" reader.

Is it a feature or a bug? Given that the reader changed even though it has the same name, i think the changed flag should be raised. If there is a technical/specification reason why it should not, I think this corner case should be documented.

If one has two readers/tokens with same reader name, the new reader/token might be something completely different.

But I still have a feeling that we should probably invalidate the reader when we get SC_ERROR_READER_REATTACHED from the reader lock as mentioned in the previous comment.

@LudovicRousseau
Copy link
Member

@Jakuje I checked on Windows and I have the same behavior than with pcsc-lite. The same PC/SC name is reused if I remove and re-insert a reader.

The SCARD_READERSTATE structure used by SCardGetStatusChange() has a dwEventState field.
This dwEventState is documented as this (for pcsc-lite):

dwEventState also contains a number of events in the upper 16 bits (dwEventState & 0xFFFF0000). This number of events is incremented for each card insertion or removal in the specified reader. This can be used to detect a card removal/insertion between two calls to SCardGetStatusChange()

I used this sample code:

from smartcard.scard import *
from smartcard.pcsc.PCSCExceptions import *
import sys


hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
if hresult != SCARD_S_SUCCESS:
    raise EstablishContextException(hresult)

hresult, readers = SCardListReaders(hcontext, [])
if hresult != SCARD_S_SUCCESS:
    raise ListReadersException(hresult)
print('PC/SC Readers:', readers)

readerstates = {}
for reader in readers:
    readerstates[reader] = (reader, SCARD_STATE_UNAWARE)
hresult, newstates = SCardGetStatusChange(hcontext, 0,
        list(readerstates.values()))
print("SCardGetStatusChange:", SCardGetErrorMessage(hresult))
for readerState in newstates:
    readername, eventstate, atr = readerState
    print("reader: {}\n state: {}\n  ATR: {}".format(readername,
                                                  hex(eventstate), atr))
    readerstates[readername] = (readername, eventstate)

print("Remove reader and press enter")
sys.stdin.read(1)

hresult, newstates = SCardGetStatusChange(hcontext, 10,
        list(readerstates.values()))
print("SCardGetStatusChange:", SCardGetErrorMessage(hresult))
for readerState in newstates:
    readername, eventstate, atr = readerState
    print("reader: {}\n state: {}\n  ATR: {}".format(readername,
                                                  hex(eventstate), atr))
    readerstates[readername] = (readername, eventstate)

hresult = SCardReleaseContext(hcontext)
print("SCardReleaseContext:", SCardGetErrorMessage(hresult))

scenario:

  1. I have a reader connected with a smard card inserted.
  2. The idea is to remove and reinsert the reader between 2 calls to SCardGetStatusChange().

I get:

$ ./remove_reader.py 
PC/SC Readers: ['Gemalto PC Twin Reader 00 00']
SCardGetStatusChange: Command successful.
reader: Gemalto PC Twin Reader 00 00
 state: 0x30022
  ATR: [59, 167, 0, 64, 24, 128, 101, 162, 8, 1, 1, 82]
Remove reader and press enter

SCardGetStatusChange: Command successful.
reader: Gemalto PC Twin Reader 00 00
 state: 0x40022
  ATR: [59, 167, 0, 64, 24, 128, 101, 162, 8, 1, 1, 82]
SCardReleaseContext: Command successful.

Note that the field state changed from 0x30022 to 0x40022.
0x22 is: SCARD_STATE_PRESENT | SCARD_STATE_CHANGED
The number of event moved from 3 to 4, so +1 event.

if I do not remove the reader I get:

PC/SC Readers: ['Gemalto PC Twin Reader 00 00']
SCardGetStatusChange: Command successful.
reader: Gemalto PC Twin Reader 00 00
 state: 0x40022
  ATR: [59, 167, 0, 64, 24, 128, 101, 162, 8, 1, 1, 82]
Remove reader and press enter

SCardGetStatusChange: Command timeout.
reader: Gemalto PC Twin Reader 00 00
 state: 0x40020
  ATR: [59, 167, 0, 64, 24, 128, 101, 162, 8, 1, 1, 82]
SCardReleaseContext: Command successful.

the state changes from 0x40022 to 0x40020.
0x20 is: SCARD_STATE_PRESENT
And the second SCardGetStatusChange() call returns SCARD_E_TIMEOUT.

So the program can know if a card has been removed (or not) between 2 SCardGetStatusChange() calls.

The results of my Python sample code are a bit different under Windows (otherwise it is not funny).
And the PC/SC layer by Apple does not provide the number of events in dwEventState

See also

@Jakuje
Copy link
Member

Jakuje commented May 17, 2023

Any idea why in my pcsc spy trace (excerpt in the previous comment #2740 (comment)) nor opensc traces above do have all the high bits zeroed? I have pcsc-lite-1.9.9 on Fedora ...

When I run your python script, I am also getting the high bytes empty:

[jjelen@t490s OpenSC ((ecd845c2f...))]$ python3 remove_reader.py
PC/SC Readers: ['Yubico YubiKey OTP+FIDO+CCID 00 00']
SCardGetStatusChange: Command successful.
reader: Yubico YubiKey OTP+FIDO+CCID 00 00
 state: 0x22
  ATR: [59, 248, 19, 0, 0, 129, 49, 254, 21, 89, 117, 98, 105, 107, 101, 121, 52, 212]
Remove reader and press enter
        
SCardGetStatusChange: Command timeout.
reader: Yubico YubiKey OTP+FIDO+CCID 00 00
 state: 0x20
  ATR: [59, 248, 19, 0, 0, 129, 49, 254, 21, 89, 117, 98, 105, 107, 101, 121, 52, 212]
SCardReleaseContext: Command successful.

Regardless if I use my yubikey or gemalto reader Gemalto PC Twin Reader (84090C9A) 00 00. What could be the difference?

@LudovicRousseau
Copy link
Member

The dwEventState high bytes counts card events.
Since you can't remove the card from the YubiKey I am not surprised it is 0.

I run the script again and I do not see the high bytes increase any more :-(
I guess I inadvertently removed the card while also removing the reader.

In fact it was a bug in pcsc-lite I just fixed in LudovicRousseau/PCSC@92080dc. The event counter was not reset when a reader is inserted. Oups!

I now get, with a reader removal/reinsertion:

PC/SC Readers: ['Gemalto PC Twin Reader 00 00']
SCardGetStatusChange: Command successful.
reader: Gemalto PC Twin Reader 00 00
 state: 0x60022
  ATR: [59, 167, 0, 64, 24, 128, 101, 162, 8, 1, 1, 82]
Remove reader and press enter

SCardGetStatusChange: Command successful.
reader: Gemalto PC Twin Reader 00 00
 state: 0x22
  ATR: [59, 167, 0, 64, 24, 128, 101, 162, 8, 1, 1, 82]
SCardReleaseContext: Command successful.

The event counter is reset to 0

@Jakuje
Copy link
Member

Jakuje commented May 17, 2023

oh, card event. Missed that. That explains it. So this wont be any use for tokens without removable cards.

@LudovicRousseau
Copy link
Member

In any case, if the application has a connection to the card/token (with SCardConnect()) then you should get an error on the next PC/SC call after the token removal/reinsertion.

In your case you have:

        SCardBeginTransaction
         i hCard: 0x7E8AF4B6
         => Invalid value given. (SCARD_E_INVALID_VALUE [0x80100011])  [0.000091]

OpenSC should then consider the card has been removed.

I agree the error message is not very informative.

I note that, on Windows, the SCardBeginTransaction() does not fail after reader removal/re-insertion. So you should also check the result of the next PC/SC call.
I have not checked under macOS.

@frankmorgner
Copy link
Member

So my question would be if the following block should handle also invalidation of the whole card structures or if this code path is likely to be hit also in some other use cases:

This is a difficult question. The easiest thing one could do to signal this problem beyond the return code of pcsc_lock would be to set SC_READER_REMOVED. This would be removed in the next run of pcsc_detect_readers or pcsc_wait_for_event (but not in plain refresh_attributes/pcsc_detect_card_presence. At least in PKCS#11, SC_READER_REMOVED would result in a full reinitialization of the reader/card. In minidriver this can be ignored, because the PCSC events are handled by the OS. In CTK this can be ignored as well, I think, because the OS handles reader events as well (and initializes or tears down the token driver i.e. OpenSCToken).

Although this should work, it creates a logical gap since we are reconnecting to and actively using the reader/card even though we are marking it as removed. From a logical point of view, I would choose to avoid automatic reconnection on the lower level (i.e. in sc_lock or pcsc_lock), but this kind of behavior has been used for decades and it touches every interaction with the card, so I am be a little unsettled about "fixing" this now..

@Jakuje
Copy link
Member

Jakuje commented May 25, 2023

My proposal would be to merge the current code and move the outstanding issue into separate issue/PR if we will get to fix that. I think the current version catches the most common cases and if we would like to catch also the last one for short-lived removals, it should be a stretch goal. What do you think?

@frankmorgner frankmorgner merged commit 839d968 into OpenSC:master May 26, 2023
@frankmorgner
Copy link
Member

Ok, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants