Skip to content

Commit 0fe68cf

Browse files
committed
Implement largeBlobKey
Also change logic to ensure RK IVs are properly used.
1 parent 24fa128 commit 0fe68cf

File tree

14 files changed

+1001
-166
lines changed

14 files changed

+1001
-166
lines changed

README.md

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
## Overview
44

5-
This repository contains sources for a FIDO2 CTAP2.1 compatible(-ish)
6-
applet targeting the Javacard Classic system, version 3.0.4. In a
5+
This repository contains sources for a feature complete, FIDO2 CTAP2.1
6+
compatible applet targeting the Javacard Classic system, version 3.0.4. In a
77
nutshell, this lets you take a smartcard, install an app onto it,
88
and have it work as a FIDO2 authenticator device with a variety of
99
features. You can generate and use OpenSSH `ecdsa-sk` type keys, including
1010
ones you carry with you on the key (`-O resident`). You can securely unlock
1111
a LUKS encrypted disk with `systemd-cryptenroll`. You can log in to a Linux
1212
system locally with [pam-u2f](https://github.com/Yubico/pam-u2f).
1313

14-
In order to run this, you will need
14+
100% of the FIDO2 CTAP2.1 spec is covered, with the exception of features
15+
that aren't physically on an ordinary smartcard, such as biometrics or
16+
other on-board user verification. The implementation is not 100% standards
17+
compliant, but you can expect very good results generally.
18+
19+
In order to run this outside a simulator, you will need
1520
[a compatible smartcard](docs/requirements.md). Some smartcards which
1621
describe themselves as running Javacard 3.0.1 also work - see the
1722
detailed requirements.
@@ -55,38 +60,37 @@ I suggest [reading the FAQ](docs/FAQ.md) and perhaps [the security model](docs/s
5560

5661
## Implementation Status
5762

58-
| Feature | Status |
59-
|-------------------------------------------|---------------------------------------------------------|
60-
| CTAP1/U2F | Implemented (see [install guide](docs/certs.md)) |
61-
| CTAP2.0 core | Implemented |
62-
| CTAP2.1 core | Implemented |
63-
| Resident keys | Implemented, default 50 slots |
64-
| User Presence | User always considered present: not standards compliant |
65-
| ECDSA (SecP256r1) | Implemented |
66-
| Self attestation | Implemented |
67-
| Basic attestation with ECDSA certs | Implemented (see [install guide](docs/certs.md)) |
68-
| Other crypto, like ed25519 | Not implemented |
69-
| CTAP2.0 hmac-secret extension | Implemented |
70-
| CTAP2.1 hmac-secret extension | Implemented |
71-
| CTAP2.1 alwaysUv option | Implemented |
72-
| CTAP2.1 credProtect option | Implemented |
73-
| CTAP2.1 PIN Protocol 1 | Implemented |
74-
| CTAP2.1 PIN Protocol 2 | Implemented |
75-
| CTAP2.1 credential management | Implemented |
76-
| CTAP2.1 enterprise attestation | Implemented but always rejected |
77-
| CTAP2.1 authenticator config | Implemented |
78-
| CTAP2.1 minPinLength extension | Implemented, zero RPID storage capacity |
79-
| CTAP2.1 credBlob extension | Implemented, discoverable creds only |
80-
| CTAP2.1 authenticatorLargeBlobs extension | Not implemented |
81-
| CTAP2.1 largeBlobKey extension | Not implemented |
82-
| CTAP2.1 bio-stuff | Not implemented (doesn't make sense in this context?) |
83-
| APDU chaining | Supported |
84-
| Extended APDUs | Supported |
85-
| Performance | Adequate (sub-3-second common operations) |
86-
| Resource consumption | Reasonably optimized for avoiding flash wear |
87-
| Bugs | Yes |
88-
| Code quality | No |
89-
| Security | Theoretical, but see "bugs" row above |
63+
| Feature | Status |
64+
|------------------------------------|---------------------------------------------------------|
65+
| CTAP1/U2F | Implemented (see [install guide](docs/certs.md)) |
66+
| CTAP2.0 core | Implemented |
67+
| CTAP2.1 core | Implemented |
68+
| Resident keys | Implemented, default 50 slots (max 255) |
69+
| User Presence | User always considered present: not standards compliant |
70+
| ECDSA (SecP256r1) | Implemented |
71+
| Self attestation | Implemented |
72+
| Basic attestation with ECDSA certs | Implemented (see [install guide](docs/certs.md)) |
73+
| Other crypto, like ed25519 | Not implemented - availability depends on hardware |
74+
| CTAP2.1 hmac-secret extension | Implemented |
75+
| CTAP2.1 alwaysUv option | Implemented |
76+
| CTAP2.1 credProtect option | Implemented |
77+
| CTAP2.1 PIN Protocol 1 | Implemented |
78+
| CTAP2.1 PIN Protocol 2 | Implemented |
79+
| CTAP2.1 credential management | Implemented |
80+
| CTAP2.1 enterprise attestation | Implemented but never provided to RPs |
81+
| CTAP2.1 authenticator config | Implemented |
82+
| CTAP2.1 minPinLength extension | Implemented, zero RPID storage capacity |
83+
| CTAP2.1 credBlob extension | Implemented, discoverable creds only |
84+
| CTAP2.1 largeBlobKey extension | Implemented |
85+
| CTAP2.1 authenticatorLargeBlobs | Implemented, default 1024 bytes storage (max 4k) |
86+
| CTAP2.1 bio-stuff | Not implemented (doesn't make sense in this context?) |
87+
| APDU chaining | Supported |
88+
| Extended APDUs | Supported |
89+
| Performance | Adequate (sub-3-second common operations) |
90+
| Resource consumption | Reasonably optimized for avoiding flash wear |
91+
| Bugs | Yes |
92+
| Code quality | No |
93+
| Security | Theoretical, but see "bugs" row above |
9094

9195
## Software Compatibility
9296

@@ -125,8 +129,11 @@ There are two compatibility issues in the table above:
125129
hardwired to use only "passkeys". If a site explicitly requests a non-discoverable credential,
126130
you will be prompted to use an NFC security key, but this is only CTAP1 and not CTAP2. There's
127131
nothing fundamentally preventing this from working on Android but the current state of Chrome
128-
and Fennec are that CTAP2 doesn't, because both use the broken Play Services library.
132+
and Fennec are that CTAP2 doesn't, because both use the broken Play Services library. It's also
133+
worth noting that if you install an untrusted attestation certificate, some implementations will
134+
reject your created U2F/CTAP1 credentials.
129135
1. Some browsers support FIDO2 in theory but only allow USB security keys - this implementation
130136
is for PC/SC, and doesn't implement USB HID, so it will only work with FIDO2
131137
implementations that can handle e.g. NFC tokens instead of being restricted to USB. This prevents,
132-
for example, Firefox on Linux from using FIDO2Applet.
138+
for example, Firefox on Linux from using FIDO2Applet. Phyiscally USB-connected smartcards are
139+
still PC/SC devices, not HID ones!

docs/FAQ.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,20 @@ authenticator was useless - in other words, a true second factor.
5050

5151
So I wrote a CTAP2 implementation that [had that property](security_model.md).
5252

53-
## You say there are "caveats" for some implementation bits. What are those?
53+
## The implementation says you're not "standards compliant". Why?
5454

5555
Well, first off, this app doesn't attempt to do a full CBOR parse, so its error
5656
statuses often aren't perfect and it's generally tolerant of invalid input.
5757

5858
Secondly, the CTAP API requires user presence detection, but there's really no
5959
way to do that on Javacard 3.0.4. We can't even use the "presence timeout"
60-
that is described in the spec for NFC devices. So you're always treated as
61-
being present, which is to some extent offset by the fact that anything real
62-
requires you type your PIN (if one is set)... Additionally, this app will not
63-
clear CTAP2.1 PIN token permissions on use.
60+
that is described in the spec for NFC devices: there's no timer! So you're
61+
always treated as being present, which is to some extent offset by the fact
62+
that anything real requires you type your PIN (if one is set)... Additionally,
63+
this app will not clear CTAP2.1 PIN token permissions on use.
6464

6565
So set a PIN, and unplug your card when you're not using it.
6666

67-
Thirdly, implementing credProtect by storing values for non-discoverable
68-
credentials is a royal pain: it would require the key's generated credential IDs
69-
to be longer than the minimum 64 bytes. Rather than do that, this implementation
70-
just rejects the creation of level-three non-discoverable credentials while a PIN
71-
is unset. Discoverable credentials are always fine, although of course you can't
72-
USE level 3 credentials without setting a PIN...
73-
74-
So, again, set a PIN.
75-
7667
Finally, the CTAP2.0 and CTAP2.1 standards are actually mutually incompatible. When
7768
a getAssertion call is made with an `allowList` given, CTAP2.0 says that the
7869
authenticator should iterate through assertions generated with the matching
@@ -116,7 +107,8 @@ It will store:
116107
- up to 32 characters of the RP ID, again AES256 encrypted
117108
- a max 64-byte-long user ID, again AES256 encrypted
118109
- the 64-byte public key associated with the credential, again AES256 encrypted
119-
- A 16-byte random IV used for encrypting the RP ID, user ID, and public key
110+
- Several 16-byte random IVs used for encrypting the RP ID, user ID, public key,
111+
large blob key, and the credential itself
120112
- the length of the RP ID, unencrypted
121113
- the length of the user ID, unencrypted
122114
- a boolean set to true on the first credential from a given RP ID, used
@@ -190,7 +182,9 @@ to be given trouble by software bugs than by your flash write endurance. Flash i
190182
writable buffer space if your smartcard has at least 2k of RAM, is only used for long request
191183
chaining if your smartcard has 1k of RAM, and is only used for "ordinary" requests if you're under
192184
around 200 bytes of RAM. Great care has been taken to make sure the most common operations like
193-
getPinToken and getKeyAgreement don't write to flash.
185+
getPinToken and getKeyAgreement don't write to flash unnecessarily.
186+
187+
Note that operations like writing the largeBlobStore will, of course, use flash.
194188

195189
If you want to assess exactly what is and is not in RAM on your particular Javacard, you can install
196190
the applet and send APDUs like the following:

docs/security_model.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ on-device wrapping key. Without doing that, they can read incidentals like:
165165
- how long each key's user ID is
166166
- how many different RPs in total have resident keys on the device
167167
- the credProtect level of each resident key
168+
- the length and contents of the stored "large blob array" (note:
169+
the FIDO standard requires that the platform encrypt the contents
170+
of the large blob array, so the authenticator implementation here
171+
does not. What can be read is what the getLargeBlobs operation
172+
returns without authentication anyhow...)
168173

169174
What they can't do without decrypting the wrapping key is get at
170175
your actual credentials for sites - the private keys, the RP IDs,

mds.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"attestationRootCertificates": [],
3535
"authenticatorGetInfo": {
3636
"versions": [ "FIDO_2_0", "FIDO_2_1", "FIDO_2_1_PRE" ],
37-
"extensions": [ "credBlob", "credProtect", "hmac-secret" ],
37+
"extensions": [ "credBlob", "credProtect", "hmac-secret", "largeBlobKey" ],
3838
"aaguid": "00000000000000000000000000000000",
3939
"options": {
4040
"ep": true,
@@ -43,6 +43,7 @@
4343
"alwaysUv": false,
4444
"credMgmt": true,
4545
"authnrCfg": true,
46+
"largeBlobs": true,
4647
"makeCredUvNotRqd": false,
4748
"pinUvAuthToken": true
4849
},

python_tests/ctap/ctap_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ def get_assertion_from_cred(self, cred: Optional[AttestationResponse],
277277
**kwargs
278278
)
279279

280-
def get_assertion(self, rp_id: str, client_data: Optional[bytes] = None):
281-
return self.get_assertion_from_cred(cred=None, rp_id=rp_id, client_data=client_data)
280+
def get_assertion(self, rp_id: str, client_data: Optional[bytes] = None, **kwargs):
281+
return self.get_assertion_from_cred(cred=None, rp_id=rp_id, client_data=client_data, **kwargs)
282282

283283
def get_high_level_client(self, extensions: Optional[list[Type[Ctap2Extension]]] = None,
284284
user_interaction: UserInteraction = None,

python_tests/ctap/test_ctap_attestation_mode_switch.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def setUp(self, install_params: Optional[bytes] = None) -> None:
1313
super().setUp(bytes([0x01]))
1414

1515
@parameterized.expand([
16+
("tiny", 3),
1617
("very short", 140),
1718
("short", 300),
1819
("medium", 900),
@@ -36,6 +37,37 @@ def test_applying_cert_len(self, _, length):
3637

3738
cred_res = self.ctap2.make_credential(**self.basic_makecred_params)
3839
self.assertEqual(self.cert, cred_res.att_stmt['x5c'][0])
40+
self.assertIsNone(cred_res.large_blob_key)
41+
42+
@parameterized.expand([
43+
("tiny", 3),
44+
("very short", 140),
45+
("short", 300),
46+
("medium", 900),
47+
("long", 2000),
48+
("very long", 8000),
49+
])
50+
def test_applying_cert_len_with_large_blob(self, _, length):
51+
info_before = self.ctap2.get_info()
52+
self.assertEqual(Aaguid.NONE, info_before.aaguid)
53+
54+
cert_bytes = secrets.token_bytes(length)
55+
cert = self.gen_attestation_cert([cert_bytes])
56+
57+
self.ctap2.send_cbor(
58+
self.VENDOR_COMMAND_SWITCH_ATT,
59+
args(cert)
60+
)
61+
62+
info_after = self.ctap2.get_info()
63+
self.assertEqual(self.aaguid, info_after.aaguid)
64+
65+
self.basic_makecred_params['options'] = {'rk': True}
66+
self.basic_makecred_params['extensions'] = {'largeBlobKey': True}
67+
cred_res = self.ctap2.make_credential(**self.basic_makecred_params)
68+
self.assertEqual(self.cert, cred_res.att_stmt['x5c'][0])
69+
self.assertIsNotNone(cred_res.large_blob_key)
70+
self.assertEqual(32, len(cred_res.large_blob_key))
3971

4072
def test_u2f_supported_after_switch(self):
4173
info_before = self.ctap2.get_info()

python_tests/ctap/test_ctap_basics.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def test_info_supported_versions(self):
1616

1717
def test_info_supported_extensions(self):
1818
info = self.ctap2.get_info()
19-
self.assertEqual(["credBlob", "credProtect", "hmac-secret"], info.extensions)
19+
self.assertEqual(["credBlob", "credProtect", "hmac-secret", "largeBlobKey"], info.extensions)
2020

2121
def test_info_aaguid_none(self):
2222
info = self.ctap2.get_info()
@@ -129,7 +129,7 @@ def test_makecred_disallowed_by_exclude_list(self):
129129
self.assertEqual(CtapError.ERR.CREDENTIAL_EXCLUDED, e2.exception.code)
130130

131131
def test_multiple_matching_rks(self):
132-
creds_to_make = 3
132+
creds_to_make = 5
133133
self.basic_makecred_params['options'] = {
134134
'rk': True
135135
}
@@ -146,9 +146,10 @@ def test_multiple_matching_rks(self):
146146
with self.assertRaises(CtapError) as e:
147147
self.ctap2.get_next_assertion()
148148
self.assertEqual(CtapError.ERR.NO_CREDENTIALS, e.exception.code)
149-
self.assertEqual(sorted([x.auth_data.credential_data.credential_id for x in creds]),
150-
sorted([x.credential['id'] for x in asserts])
151-
)
149+
self.assertEqual(
150+
[x.auth_data.credential_data.credential_id for x in creds][::-1],
151+
[x.credential['id'] for x in asserts]
152+
)
152153

153154
def test_makecred_rk_disallowed_by_exclude_list(self):
154155
non_resident_cred = self.ctap2.make_credential(**self.basic_makecred_params)

0 commit comments

Comments
 (0)