Skip to content

Commit ff06f8b

Browse files
committed
Support up to 32767 discoverable credentials
1 parent e8e8f39 commit ff06f8b

4 files changed

Lines changed: 115 additions & 55 deletions

File tree

python_tests/ctap/ctap_test.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -647,13 +647,14 @@ def setUp(self, install_params: Optional[bytes] = None) -> None:
647647
self.pin = secrets.token_hex(10)
648648
ClientPin(self.ctap2).set_pin(self.pin)
649649

650-
def get_credential_management(self, permissions: Optional[int] = None) -> CredentialManagement:
650+
def get_credential_management(self, permissions: Optional[int] = None, client = None) -> CredentialManagement:
651651
if permissions is None:
652652
permissions = self.PERMISSION_CRED_MGMT
653653

654-
client = self.get_high_level_client(
655-
user_interaction=FixedPinUserInteraction(self.pin),
656-
)
654+
if client is None:
655+
client = self.get_high_level_client(
656+
user_interaction=FixedPinUserInteraction(self.pin),
657+
)
657658
# noinspection PyTypeChecker
658659
be: _Ctap2ClientBackend = client._backend
659660
token = be._get_token(ClientPin(self.ctap2), permissions=permissions,

python_tests/ctap/test_cred_mgmt.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,59 @@ def test_rk_overwrite_multiple_creds(self):
249249
self.assertEqual(1, len(rps))
250250
creds = cm.enumerate_creds(rp_id_hash=self.rp_id_hash(self.rp_id))
251251
self.assertEqual(2, len(creds))
252+
253+
def test_many_rks_same_rp(self):
254+
pin_client = self.get_high_level_client(user_interaction=FixedPinUserInteraction(self.pin))
255+
self.basic_makecred_params['options'] = {
256+
'rk': True
257+
}
258+
259+
NUM_CREDS_TO_TEST = 200
260+
261+
results = []
262+
263+
for i in range(NUM_CREDS_TO_TEST):
264+
user_id = secrets.token_bytes(30)
265+
res = pin_client.make_credential(
266+
self.get_high_level_make_cred_options(
267+
ResidentKeyRequirement.REQUIRED, user_id=user_id
268+
)
269+
)
270+
results.append(res)
271+
272+
cm = self.get_credential_management()
273+
rps = cm.enumerate_rps()
274+
self.assertEqual(1, len(rps))
275+
creds = cm.enumerate_creds(rp_id_hash=self.rp_id_hash(self.rp_id))
276+
self.assertEqual(len(results), len(creds))
277+
278+
pin_client.get_assertion(self.get_high_level_assertion_opts_from_cred(cred=results[len(results)//2], rp_id=self.rp_id))
279+
280+
def test_many_rps(self):
281+
self.basic_makecred_params['options'] = {
282+
'rk': True
283+
}
284+
285+
NUM_CREDS_TO_TEST = 200
286+
287+
results = []
288+
289+
for i in range(NUM_CREDS_TO_TEST):
290+
user_id = secrets.token_bytes(30)
291+
rp_id = secrets.token_hex(20)
292+
pin_client = self.get_high_level_client(user_interaction=FixedPinUserInteraction(self.pin), origin='https://' + rp_id)
293+
res = pin_client.make_credential(
294+
self.get_high_level_make_cred_options(
295+
ResidentKeyRequirement.REQUIRED, user_id=user_id,
296+
rp_id=rp_id
297+
)
298+
)
299+
results.append(res)
300+
301+
pin_client = self.get_high_level_client(
302+
user_interaction=FixedPinUserInteraction(self.pin),
303+
origin=''
304+
)
305+
cm = self.get_credential_management(client=pin_client)
306+
rps = cm.enumerate_rps()
307+
self.assertEqual(len(results), len(rps))

src/main/java/us/q3q/fido2/FIDO2Applet.java

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,11 @@ public final class FIDO2Applet extends Applet implements ExtendedLength {
377377
/**
378378
* How many resident key slots are filled
379379
*/
380-
private byte numResidentCredentials;
380+
private short numResidentCredentials;
381381
/**
382382
* How many distinct RPs are present across all resident keys
383383
*/
384-
private byte numResidentRPs;
384+
private short numResidentRPs;
385385
/**
386386
* Storage for the largeBlobs extension
387387
*/
@@ -1050,7 +1050,7 @@ private void makeCredential(APDU apdu, short lc, byte[] buffer) {
10501050
boolean uniqueRP = false;
10511051
if (!foundMatchingRK) {
10521052
// We're filling an empty slot
1053-
numResidentCredentials++;
1053+
numResidentCredentials = (short)(numResidentCredentials + 1);
10541054
if (!foundRPMatchInRKs) {
10551055
uniqueRP = true;
10561056
}
@@ -1556,7 +1556,7 @@ private short checkIfPubKeyBlockSupported(APDU apdu, byte[] buffer, short readId
15561556
sendErrorByte(apdu, FIDOConstants.CTAP2_ERR_CBOR_UNEXPECTED_TYPE);
15571557
}
15581558
short valLen = (short)(typeB - 0x60);
1559-
readIdx += valLen + 1;
1559+
readIdx += (short)(valLen + 1);
15601560
if (readIdx >= lc) {
15611561
sendErrorByte(apdu, FIDOConstants.CTAP2_ERR_INVALID_CBOR);
15621562
}
@@ -1574,7 +1574,7 @@ private short checkIfPubKeyBlockSupported(APDU apdu, byte[] buffer, short readId
15741574
// Not of type "public-key", although same length
15751575
transientStorage.readyStoredVars();
15761576
}
1577-
readIdx += typeValLen + 1;
1577+
readIdx += (short)(typeValLen + 1);
15781578
if (readIdx >= lc) {
15791579
sendErrorByte(apdu, FIDOConstants.CTAP2_ERR_INVALID_CBOR);
15801580
}
@@ -1941,7 +1941,7 @@ private void getAssertion(final APDU apdu, final short lc, final byte[] buffer,
19411941
boolean acceptedMatch = false;
19421942

19431943
short hmacSecretBytes = 0;
1944-
byte numMatchesThisRP = 0;
1944+
short numMatchesThisRP = 0;
19451945
short rkMatch = -1;
19461946
short allowListLength = 0;
19471947

@@ -2964,11 +2964,7 @@ private boolean checkCredential(APDU apdu, byte[] credentialBuffer, short creden
29642964
}
29652965
}
29662966

2967-
if (!matches) {
2968-
return false;
2969-
}
2970-
2971-
return true;
2967+
return matches;
29722968
}
29732969

29742970
/**
@@ -4988,10 +4984,11 @@ private void handleDeleteCred(APDU apdu, byte[] buffer, short readOffset, short
49884984
} else {
49894985
residentKeys[rpHavingSameRP].setUniqueRP(true);
49904986
}
4991-
for (short j = i; j < (short)(numResidentCredentials - 1); j++) {
4987+
numResidentCredentials = (short)(numResidentCredentials - 1);
4988+
for (short j = i; j < numResidentCredentials; j++) {
49924989
residentKeys[j] = residentKeys[(short)(j + 1)];
49934990
}
4994-
residentKeys[--numResidentCredentials] = null;
4991+
residentKeys[numResidentCredentials] = null;
49954992
ok = true;
49964993
} finally {
49974994
if (ok) {
@@ -5004,10 +5001,11 @@ private void handleDeleteCred(APDU apdu, byte[] buffer, short readOffset, short
50045001
JCSystem.beginTransaction();
50055002
boolean ok = false;
50065003
try {
5007-
for (short j = i; j < (short)(numResidentCredentials - 1); j++) {
5004+
numResidentCredentials = (short)(numResidentCredentials - 1);
5005+
for (short j = i; j < numResidentCredentials; j++) {
50085006
residentKeys[j] = residentKeys[(short)(j + 1)];
50095007
}
5010-
residentKeys[--numResidentCredentials] = null;
5008+
residentKeys[numResidentCredentials] = null;
50115009
ok = true;
50125010
} finally {
50135011
if (ok) {
@@ -5039,7 +5037,7 @@ private void handleDeleteCred(APDU apdu, byte[] buffer, short readOffset, short
50395037
* If zero, we're starting a new iteration
50405038
* @param lc Length of the incoming request, as sent by the platform
50415039
*/
5042-
private void handleEnumerateCreds(APDU apdu, byte[] buffer, short bufferIdx, short startCredIdx, short lc) {
5040+
private void handleEnumerateCreds(APDU apdu, byte[] buffer, short bufferIdx, final short startCredIdx, short lc) {
50435041
transientStorage.clearIterationPointers();
50445042

50455043
if (startCredIdx > (short) residentKeys.length) { // intentional > instead of >=
@@ -5099,7 +5097,7 @@ private void handleEnumerateCreds(APDU apdu, byte[] buffer, short bufferIdx, sho
50995097
rpIdHashBuf, credIdIdx, (byte) 3)) {
51005098
// Cred is for this RP ID, yay.
51015099

5102-
byte matchingCount = 1; // remember to count THIS cred as a match
5100+
short matchingCount = 1; // remember to count THIS cred as a match
51035101
if (startCredIdx == 0) {
51045102
// Unfortunately, we need to scan forward through all remaining credentials
51055103
// we're not storing a list of which creds share an RP, so this is the only way to get
@@ -5119,7 +5117,7 @@ private void handleEnumerateCreds(APDU apdu, byte[] buffer, short bufferIdx, sho
51195117
}
51205118
}
51215119
}
5122-
transientStorage.setCredIterationPointer((byte)(rkIndex + 1)); // resume iteration from beyond this one
5120+
transientStorage.setCredIterationPointer((short)(rkIndex + 1)); // resume iteration from beyond this one
51235121

51245122
byte[] outBuf = bufferMem;
51255123

@@ -5231,15 +5229,15 @@ private short packCredentialId(byte[] credBuffer, short credOffset, byte[] write
52315229
* This is, unfortunately, O(N^2) in the number of unique RPs.
52325230
*/
52335231
private void updateRKStatekeeping(APDU apdu) {
5234-
short rp2Handle = bufferManager.allocate(apdu, CREDENTIAL_ID_LEN, BufferManager.ANYWHERE);
5235-
byte[] rp2Buffer = bufferManager.getBufferForHandle(apdu, rp2Handle);
5236-
short rp2Offset = bufferManager.getOffsetForHandle(rp2Handle);
5232+
final short rp2Handle = bufferManager.allocate(apdu, CREDENTIAL_ID_LEN, BufferManager.ANYWHERE);
5233+
final byte[] rp2Buffer = bufferManager.getBufferForHandle(apdu, rp2Handle);
5234+
final short rp2Offset = bufferManager.getOffsetForHandle(rp2Handle);
52375235

5238-
short rp1Handle = bufferManager.allocate(apdu, RP_HASH_LEN, BufferManager.ANYWHERE);
5239-
byte[] rp1Buffer = bufferManager.getBufferForHandle(apdu, rp1Handle);
5240-
short rp1Offset = bufferManager.getOffsetForHandle(rp1Handle);
5236+
final short rp1Handle = bufferManager.allocate(apdu, RP_HASH_LEN, BufferManager.ANYWHERE);
5237+
final byte[] rp1Buffer = bufferManager.getBufferForHandle(apdu, rp1Handle);
5238+
final short rp1Offset = bufferManager.getOffsetForHandle(rp1Handle);
52415239

5242-
byte numUniqueRPsFound = 0;
5240+
short numUniqueRPsFound = 0;
52435241

52445242
for (short rkIndex1 = 0; rkIndex1 < (short) residentKeys.length; rkIndex1++) {
52455243
if (residentKeys[rkIndex1] == null) {
@@ -5328,7 +5326,7 @@ private void handleEnumerateRPs(APDU apdu, short startOffset) {
53285326

53295327
outBuf[writeOffset++] = FIDOConstants.CTAP2_OK;
53305328

5331-
transientStorage.setRPIterationPointer((byte)(rkIndex + 1));
5329+
transientStorage.setRPIterationPointer((short)(rkIndex + 1));
53325330

53335331
outBuf[writeOffset++] = isContinuation ? (byte) 0xA2 : (byte) 0xA3; // map with two or three keys
53345332
outBuf[writeOffset++] = 0x03; // map key: rp
@@ -5407,12 +5405,15 @@ private void handleCredentialManagementGetCredsMetadata(APDU apdu) {
54075405
*
54085406
* @return New write offset into given buffer
54095407
*/
5410-
private short encodeIntTo(byte[] outBuf, short writeOffset, byte v) {
5408+
private short encodeIntTo(byte[] outBuf, short writeOffset, short v) {
54115409
if (v < 24) {
5412-
outBuf[writeOffset++] = v;
5413-
} else {
5410+
outBuf[writeOffset++] = (byte) v;
5411+
} else if (v < 256) {
54145412
outBuf[writeOffset++] = 0x18; // Integer stored in one byte
5415-
outBuf[writeOffset++] = v;
5413+
outBuf[writeOffset++] = (byte) v;
5414+
} else {
5415+
outBuf[writeOffset++] = 0x19; // Integer stored in two bytes
5416+
writeOffset = Util.setShort(outBuf, writeOffset, v);
54165417
}
54175418
return writeOffset;
54185419
}
@@ -6421,7 +6422,7 @@ private short consumeKeyAgreement(APDU apdu, byte[] buffer, short readIdx, byte
64216422
CannedCBOR.PUBLIC_KEY_DH_ALG_PREAMBLE, (short) 0, (short) CannedCBOR.PUBLIC_KEY_DH_ALG_PREAMBLE.length) != 0) {
64226423
sendErrorByte(apdu, FIDOConstants.CTAP2_ERR_UNSUPPORTED_ALGORITHM);
64236424
}
6424-
readIdx += CannedCBOR.PUBLIC_KEY_DH_ALG_PREAMBLE.length;
6425+
readIdx += (short) CannedCBOR.PUBLIC_KEY_DH_ALG_PREAMBLE.length;
64256426

64266427
short xIdx = readIdx;
64276428
readIdx += KEY_POINT_LENGTH;

src/main/java/us/q3q/fido2/TransientStorage.java

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,31 @@ public final class TransientStorage {
3535
* Cred or RP iteration index, used when iterating through creds or RPs with credManagement commands
3636
* Disambiguated by the most significant bit: 1 for RPs, 0 for creds
3737
*/
38-
private static final byte IDX_CRED_RP_ITERATION_POINTER = 5; // 1 byte
38+
private static final byte IDX_CRED_RP_ITERATION_POINTER = 5; // 2 bytes
3939
/**
4040
* Index of next credential to consider when iterating through assertions with getNextAssertion commands
4141
*/
42-
private static final byte IDX_ASSERT_ITERATION_POINTER = 6; // 1 byte
42+
private static final byte IDX_ASSERT_ITERATION_POINTER = 7; // 1 byte
4343
/**
4444
* When writing an overlong response using chained APDUs, stores the position we're up to in the outgoing buffer
4545
*/
46-
private static final byte IDX_CONTINUATION_OUTGOING_WRITE_OFFSET = 7; // 2 bytes
46+
private static final byte IDX_CONTINUATION_OUTGOING_WRITE_OFFSET = 8; // 2 bytes
4747
/**
4848
* When writing an overlong response using chained APDUs, stores the remaining bytes in the outgoing buffer
4949
*/
50-
private static final byte IDX_CONTINUATION_OUTGOING_REMAINING = 9; // 2 bytes
50+
private static final byte IDX_CONTINUATION_OUTGOING_REMAINING = 10; // 2 bytes
5151
/**
5252
* When reading an overlong incoming request using chained APDUs, stores the fill level of the incoming buffer
5353
*/
54-
private static final byte IDX_CHAINING_INCOMING_READ_OFFSET = 11; // 2 bytes
54+
private static final byte IDX_CHAINING_INCOMING_READ_OFFSET = 12; // 2 bytes
5555
/**
5656
* Giant boolean bitfield that holds all the BOOL_IDX variables below
5757
*/
58-
private static final byte IDX_BOOLEAN_OMNIBUS = 13; // 1 byte
58+
private static final byte IDX_BOOLEAN_OMNIBUS = 14; // 1 byte
5959
/**
6060
* How many bytes long the temp storage should be
6161
*/
62-
private static final byte NUM_RESET_BYTES = 14;
62+
private static final byte NUM_RESET_BYTES = 15;
6363

6464
// boolean bit indices held in BOOLEAN_OMNIBUS byte above
6565
/**
@@ -172,7 +172,7 @@ public short getLargeBlobWriteTotalLength() {
172172
}
173173

174174
public void clearIterationPointers() {
175-
tempBytes[IDX_CRED_RP_ITERATION_POINTER] = 0;
175+
Util.setShort(tempBytes, IDX_CRED_RP_ITERATION_POINTER, (short) 0);
176176
}
177177

178178
public void clearAssertIterationPointer() {
@@ -304,26 +304,28 @@ public void setResetCommandSentSincePowerOn() {
304304
setBoolByIdx(BOOL_IDX_RESET_RECEIVED_SINCE_POWERON, true);
305305
}
306306

307-
public byte getRPIterationPointer() {
308-
if ((tempBytes[IDX_CRED_RP_ITERATION_POINTER] & 0x80) == 0) {
309-
return 0x00;
307+
public short getRPIterationPointer() {
308+
final short ret = Util.getShort(tempBytes, IDX_CRED_RP_ITERATION_POINTER);
309+
if (ret < 0) {
310+
return (short)(ret * -1);
310311
}
311-
return (byte)(tempBytes[IDX_CRED_RP_ITERATION_POINTER] & 0x7F);
312+
return ret;
312313
}
313314

314-
public void setRPIterationPointer(byte val) {
315-
tempBytes[IDX_CRED_RP_ITERATION_POINTER] = (byte)(val | 0x80);
315+
public void setRPIterationPointer(short val) {
316+
Util.setShort(tempBytes, IDX_CRED_RP_ITERATION_POINTER, (short)(val * -1));
316317
}
317318

318-
public byte getCredIterationPointer() {
319-
if ((tempBytes[IDX_CRED_RP_ITERATION_POINTER] & 0x80) != 0) {
320-
return 0x00;
319+
public short getCredIterationPointer() {
320+
final short ret = Util.getShort(tempBytes, IDX_CRED_RP_ITERATION_POINTER);
321+
if (ret > 0) {
322+
return ret;
321323
}
322-
return tempBytes[IDX_CRED_RP_ITERATION_POINTER];
324+
return 0;
323325
}
324326

325-
public void setCredIterationPointer(byte val) {
326-
tempBytes[IDX_CRED_RP_ITERATION_POINTER] = val;
327+
public void setCredIterationPointer(short val) {
328+
Util.setShort(tempBytes, IDX_CRED_RP_ITERATION_POINTER, val);
327329
}
328330

329331
public void setPinProtocolInUse(byte pinProtocol, byte pinPermissions) {

0 commit comments

Comments
 (0)