| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
When enumerating resident credentials via ENUMERATE_CREDS_BEGIN followed
by one or more ENUMERATE_CREDS_NEXT calls, the credential iteration
pointer could be silently cleared mid-enumeration if the BEGIN response
required more than one GET RESPONSE exchange to be fully delivered.
Root cause
----------
streamOutgoingContinuation() is called for each GET RESPONSE APDU
(INS=0xC0) the platform sends to retrieve a chained response. When a
chunk is sent and further data remains, the method signals this by
calling throwException() with SW=61xx. However, it used the single-
argument overload of throwException(), which defaults to
clearIteration=true and unconditionally calls
transientStorage.clearIterationPointers() before throwing the ISO
exception.
This meant that every intermediate GET RESPONSE that still had bytes
remaining would destroy the credential iteration pointer that had just
been set by handleEnumerateCreds(). A concrete example with a ~523-byte
ENUMERATE_CREDS_BEGIN response:
1. handleEnumerateCreds() sets credIterationPointer = 1
2. doSendResponse() sends 256 bytes, calls setupChainedResponse()
with clearIteration=false → pointer preserved (= 1)
3. Platform sends GET RESPONSE BryanJacobs#1 → streamOutgoingContinuation()
sends 256 bytes, 11 remain → throwException(SW=610b)
[single-arg → clearIteration=true] → pointer CLEARED (= 0)
4. Platform sends GET RESPONSE BryanJacobs#2 → streamOutgoingContinuation()
sends final 11 bytes → SW=9000
5. Platform sends ENUMERATE_CREDS_NEXT → credPtr == 0 →
OPERATION_DENIED (0x27)
This caused silent data loss: a client receiving OPERATION_DENIED on
ENUMERATE_CREDS_NEXT would break out of its enumeration loop early,
dropping all subsequent credentials for the current RP. Only credentials
returned by BEGIN were visible; all NEXT calls would fail.
The bug did not affect clients using extended-length APDUs,
because doSendResponse() detects isExtendedAPDU
and streams the entire response in a single burst via
streamOutgoingContinuation(chaining=false), which never calls
throwException() at all.
Fix
---
Change both throwException() calls in the chaining branch of
streamOutgoingContinuation() to use the two-argument overload with
clearIteration=false. GET RESPONSE is a continuation of an already-
initiated command response, not a new command; it must never disturb
enumeration state.
- throwException(ISO7816.SW_BYTES_REMAINING_00, false)
- throwException((short)(ISO7816.SW_BYTES_REMAINING_00 + remaining), false)
The first call (remaining >= 256) was already using false in some
versions; both are made explicit here for consistency and correctness.
|
For the context: I am working on a modified version of Yubikey Manager adapted to support smartcard devices with the FIDO2 applet. When listing credentials using ykman fido credentials list, I get the fido2.ctap.CtapError: CTAP error: 0x27 - OPERATION_DENIED error. Using this patch seems to correct this issue and correctly list all the resident passkeys stored in the card. |
Sorry, something went wrong.
|
Good find. This looks right to me. Thanks! |
Sorry, something went wrong.
This commit is ported from upstream repo: BryanJacobs/FIDO2Applet#55 Original commit description: When enumerating resident credentials via ENUMERATE_CREDS_BEGIN followed by one or more ENUMERATE_CREDS_NEXT calls, the credential iteration pointer could be silently cleared mid-enumeration if the BEGIN response required more than one GET RESPONSE exchange to be fully delivered. Root cause ---------- streamOutgoingContinuation() is called for each GET RESPONSE APDU (INS=0xC0) the platform sends to retrieve a chained response. When a chunk is sent and further data remains, the method signals this by calling throwException() with SW=61xx. However, it used the single- argument overload of throwException(), which defaults to clearIteration=true and unconditionally calls transientStorage.clearIterationPointers() before throwing the ISO exception. This meant that every intermediate GET RESPONSE that still had bytes remaining would destroy the credential iteration pointer that had just been set by handleEnumerateCreds(). A concrete example with a ~523-byte ENUMERATE_CREDS_BEGIN response: 1. handleEnumerateCreds() sets credIterationPointer = 1 2. doSendResponse() sends 256 bytes, calls setupChainedResponse() with clearIteration=false → pointer preserved (= 1) 3. Platform sends GET RESPONSE token2#1 → streamOutgoingContinuation() sends 256 bytes, 11 remain → throwException(SW=610b) [single-arg → clearIteration=true] → pointer CLEARED (= 0) 4. Platform sends GET RESPONSE token2#2 → streamOutgoingContinuation() sends final 11 bytes → SW=9000 5. Platform sends ENUMERATE_CREDS_NEXT → credPtr == 0 → OPERATION_DENIED (0x27) This caused silent data loss: a client receiving OPERATION_DENIED on ENUMERATE_CREDS_NEXT would break out of its enumeration loop early, dropping all subsequent credentials for the current RP. Only credentials returned by BEGIN were visible; all NEXT calls would fail. The bug did not affect clients using extended-length APDUs, because doSendResponse() detects isExtendedAPDU and streams the entire response in a single burst via streamOutgoingContinuation(chaining=false), which never calls throwException() at all. Fix --- Change both throwException() calls in the chaining branch of streamOutgoingContinuation() to use the two-argument overload with clearIteration=false. GET RESPONSE is a continuation of an already- initiated command response, not a new command; it must never disturb enumeration state. throwException(ISO7816.SW_BYTES_REMAINING_00, false) throwException((short)(ISO7816.SW_BYTES_REMAINING_00 + remaining), false) The first call (remaining >= 256) was already using false in some versions; both are made explicit here for consistency and correctness. Original author: Toporin https://github.com/Toporin Signed-off-by: Shengyu Qu <wiagn233@outlook.com>
| Back | FazBrowse Home | New Git URL |
When enumerating resident credentials via ENUMERATE_CREDS_BEGIN followed by one or more ENUMERATE_CREDS_NEXT calls, the credential iteration pointer could be silently cleared mid-enumeration if the BEGIN response required more than one GET RESPONSE exchange to be fully delivered.
Root cause
streamOutgoingContinuation() is called for each GET RESPONSE APDU (INS=0xC0) the platform sends to retrieve a chained response. When a chunk is sent and further data remains, the method signals this by calling throwException() with SW=61xx. However, it used the single- argument overload of throwException(), which defaults to clearIteration=true and unconditionally calls transientStorage.clearIterationPointers() before throwing the ISO exception.
This meant that every intermediate GET RESPONSE that still had bytes remaining would destroy the credential iteration pointer that had just been set by handleEnumerateCreds(). A concrete example with a ~523-byte ENUMERATE_CREDS_BEGIN response:
This caused silent data loss: a client receiving OPERATION_DENIED on ENUMERATE_CREDS_NEXT would break out of its enumeration loop early, dropping all subsequent credentials for the current RP. Only credentials returned by BEGIN were visible; all NEXT calls would fail.
The bug did not affect clients using extended-length APDUs, because doSendResponse() detects isExtendedAPDU
and streams the entire response in a single burst via streamOutgoingContinuation(chaining=false), which never calls throwException() at all.
Fix
Change both throwException() calls in the chaining branch of streamOutgoingContinuation() to use the two-argument overload with clearIteration=false. GET RESPONSE is a continuation of an already- initiated command response, not a new command; it must never disturb enumeration state.