Wed 03 January 2024 Nils Amiet

security-keys passkeys passwordless

User authentication has long been relying on passwords. However, passwords come with a ton of problems. They're hard to remember, they can be reused across websites and services and are sensitive to phishing and data leaks. Also, password policies are a major pain point for users.

Password managers solve the "hard to remember" piece, but are just a workaround. They're not solving the actual problem. We need to get rid of passwords.

Passkeys

You probably have heard of Passkeys. They're a new way to sign in to websites and applications without passwords. Passkeys can be stored on your laptop, on your smartphone or on a hardware security key, such as a Yubikey.

For the average user, storing one's passkeys in their smartphone and having them synced in Google's or Apple's cloud is probably the most user-friendly solution and is what I expect most people will do in the near future.

However, this solution comes at a cost. You give up some security because you trust Google, Apple or whatever third-party passkey provider you choose to store your passkeys. This is where hardware-based security keys shine, because the passkeys you store into those can never leave the security key. They remain offline and you keep control or your passkeys.

Security keys

Shopping for security keys these days is hard. Vendors like Yubico, Google, Nitrokey and SoloKeys to name a few, make it especially hard for you to know what you're getting.

Indeed, these security keys come with a wide array of features, like FIDO2, PGP, PIV, OTP, and more. For passkeys, we only care about FIDO2 support because Passkeys are just FIDO2 credentials, and this is the topic of this blog post so we'll only cover that today.

Indeed, 99% of security key vendors fail to provide detailed technical specifications of what their FIDO2 security key supports, so you never know what you're getting and may be disappointed upon delivery.

For example, in November last year, Google announced the latest iteration of their Titan Security Key and claimed it can store up to 250 unique passkeys. This sounded super nice because current models on the market previously only allowed to store up to 25, 50 or maybe 100 passkeys. Before that, no product ever allowed to store that many passkeys.

I decided to buy that new Titan Security Key because of that claim, and it arrived the next day. When it arrived, I immediately plugged it in and ran ctapcli info to see what FIDO2 features and options it supported:

$ ctapcli info
Get the Authenticator infomation.

Get all data.
- versions                           = ["FIDO_2_0", "U2F_V2"]
- extensions                         = ["credProtect", "hmac-secret"]
- aaguid(16)                         = 42B4FB4A286643B29BF76C6669C2E5D3
- options                            = [("rk", true), ("clientPin", true)]
- max_msg_size                       = 2200
- pin_uv_auth_protocols              = [1]
- max_credential_count_in_list       = 0
- max_credential_id_length           = 0
- transports                         = []
- algorithms                         = []
- max_serialized_large_blob_array    = 0
- force_pin_change                   = false
- min_pin_length                     = 0
- firmware_version                   = 0
- max_cred_blob_length               = 0
- max_rpids_for_set_min_pin_length   = 0
- preferred_platform_uv_attempts     = 0
- uv_modality                        = 0
- remaining_discoverable_credentials = 0

Error: Invalid item

And here's the output of fido2-token:

$ fido2-token -L
/dev/hidraw0: vendor=0x18d1, product=0x9470 (Google Titan Security Key v2)
$ fido2-token -I /dev/hidraw0 
proto: 0x02
major: 0x02
minor: 0x00
build: 0x03
caps: 0x05 (wink, cbor, msg)
version strings: FIDO_2_0, U2F_V2
extension strings: credProtect, hmac-secret
aaguid: 42b4fb4a286643b29bf76c6669c2e5d3
options: rk, clientPin
fwversion: 0x0
maxmsgsiz: 2200
maxcredcntlst: 0
maxcredlen: 0
maxlargeblob: 0
pin protocols: 1
pin retries: 8
pin change required: false
uv retries: undefined

As you can see, the new Titan Security Key (2023 model) doesn't support credential management. Indeed, it may be able to store up to 250 resident credentials, but there's no way to selectively delete or enumerate those credentials. This is a big letdown for me because when I do fill those 250 slots, the only way for me to make some free space is to completely reset the key and lose all my passkeys, which is definitely not what I'd like.

I also recently purchased a Security Key C NFC by Yubico. This model can only store up to 25 passkeys, but it does support credential management:

$ ctapcli info
...
- options                            = [("rk", true), ("up", true), ("plat", false), ("clientPin", true), ("credentialMgmtPreview", true)]

Indeed, it supports enumerating resident credentials and tells how many free slots are left:

$ ctapcli cred
PIN: 

Enumerate discoverable credentials.
- existing discoverable credentials: 1/24
- rp: (id: github.com, name: )
  - credential: (id: ***, name: ***, display_name: ***)

We can see that I have one passkey stored and 24 free slots left, so the Security Key C NFC really does support storing up to 25 passkeys. What's nice about this model, is that it also reports that it supports both ECDSA with the P-256 curve, and EdDSA for signatures. I still do wish that Yubico makes a newer model with the same features but with more storage for passkeys.

Features to look for when choosing a security key

I was really disappointed when I found out that the security key I bought didn't support what I thought was a very important feature. There was no way to tell whether it supported it or not. The official product store page didn't list any detailed technical specifications. To help you avoid ending up in the same uncomfortable situation I found myself in, here's a quick guide to buying a new security key.

When I'm shopping for a new security key, this is what I look for.

1) FIDO2 support

The security key must support FIDO2 to be able to store passkeys.

2) How you're going to connect the security key

Does it support USB type A or USB type C? Does it support NFC?

3) Max number of resident credentials

Is that 25? 50? 250? Think of how many passkeys you'll need to store. Is this going to be enough?

This information may not even be shown on the vendor's official site. Sometimes, the best way to make sure, is to ask someone who already owns that security key model to check that for you. You may as well find the appendix of this blog post useful, because it contains the output of authenticatorGetInfo for multiple FIDO2 security key models I own. Maybe the one you're interested in is in that list.

4) hmac-secret extension support

This CTAP extension is required by some applications, such as LUKS. Look for the hmac-secret value in the extensions field of the CTAP authenticatorGetInfo command output.

5) credProtect extension support

The credProtect extension is used to enforce per-credential policies. During creation of a new credential (or passkey), the credProtect extension can be set to, for example, userVerificationRequired. If the authenticator supports the credProtect extension, then this passkey can only ever be used with user verification (PIN, fingerprint, etc.). This gives you the peace of mind that even if your security key is stolen, your passkeys cannot be used without your PIN or fingerprint.

6) CTAP 2.1 support

The security key implements CTAP 2.1 or higher. This is usually signaled by the presence of either the FIDO_2_1_PRE or the FIDO_2_1 value in the versions field of the CTAP authenticatorGetInfo command output. There are security vulnerabilities that can be exploited in CTAP 2.0, such as PIN bypasses, especially with respect to the use of the hmac-secret extension. You definitely want this if you're going to use a security key to unlock LUKS partitions.

7) Credential management support

Look for the presence of the credMgmt or credentialMgmtPreview value in the options field of the CTAP authenticatorGetInfo command output. Without credential management support, there's no way to delete individual passkeys and free up space. When the security key gets full, the only option is to reset it, and therefore lose all passkeys stored on it. Credential management also allows for listing stored credentials and seeing how many free slots are left.

Storing resident credentials is the only option for username-less sign in. If the credentials are not stored on the authenticator, the website you're signing in to has no way to know your username, and you'll have to manually type it. When storing resident credentials, those credentials become discoverable, and can be enumerated. Therefore, the username can be autofilled and your web browser will show you a nice UI where you can select which account you'd like to use in case you have used Passkeys for multiple accounts on a same website. For example, if you have more than one Google account.

8) Does it support biometrics?

Do you need to perform user verification with your fingerprint? Usually, PIN is supported, but some security keys have a fingerprint reader and can store your fingerprint. One advantage of using biometrics is that user presence (where you tap the security key) and user verification (where the user's identity is verified) can be performed in a single gesture: by placing your finger on the fingerprint reader. Whereas with a PIN, user presence must still be done by touching the security key and the PIN has to be entered in another step.

If a fingerprint reader sounds interesting, you may want to verify how many fingerprints can be stored on that security key. It may not be possible to enroll and store fingerprints for all your fingers. Some models only allow to store a maximum of 3 fingerprints, for example, and as usual, this number is never specified. You discover it after having purchased the security key.

To my knowledge, there is no command that will directly tell how many fingerprints can be stored. However, when enrolling a new fingerprint, the command will fail when there are no more free slots.

Note that usually, if there's a fingerprint reader, there's no NFC support, so you'll have to make a choice here.

9) Do you care about hardware attacks?

Is a physical attack in your threat model? Do you need to be covered in case your security key is stolen by an attacker? In that case, you need to make sure your security key uses a secure element to store credentials, and that there are no known vulnerabilities on that secure element.

Also, make sure that the secure element is actually used. Some vendors sell security keys that contain a secure element but which is not used at all (see the IMPORTANT NOTE on that page).

Summary and other criteria

To summarize, mainly look for the following things:

  • Does it support FIDO2?
  • USB Type-A or Type-C?
  • NFC support?
  • Max resident credentials
  • hmac-secret extension support
  • credProtect extension support
  • CTAP 2.1 support
  • Credential management support
  • Biometrics
  • Secure element

But there are a few more things also worth looking for:

  • Price
  • Form factor / design (sticks out of laptop or fits flush, slim/stylish or bulky)
  • Availability and quality of documentation
  • Product certifications, such as FIPS 140-2, NIST SP800-63B or CC EAL (you may need this for enterprise scenarios)
  • Other supported applications (OTP, PGP, static password, PIV)
  • Open source firmware
  • Upgradable firmware
  • Does it work inside virtual machines? (this is a pretty niche use-case, but we've seen models work better than others inside VMs with USB passthrough)

So which key is the best on the market today?

I'm not affiliated with Yubico, but I do believe they make pretty good, secure and affordable products, that come with good documentation. Especially, after having used security keys made by other vendors.

They may not list all the technical details of their products on their website (see the appendix for more info), but I can easily recommend the $25 Security Key C NFC if Passkeys are all you care about and it's for personal use. I would also recommend buying a pair of these and register both security keys as a backup on all the websites you use, in case you lose one so that you don't get locked out. The only downside I see is the number of resident credentials is limited to 25. Hopefully they release a new model that bumps that number sooner than later.

If you need more applications like PGP, OTP or PIV, I would go for the Yubikey 5, which is essentially the same thing, but with more applications and also for double the price. But these days, I think Passkeys/FIDO2 is the killer app. Do you really need support for those other applications?

Conclusions

For the average person, maybe a security key is overkill and storing passkeys in their smartphone, plus cloud-sync is enough. But if you're concerned about third-parties storing your passkeys, this blog post gave an overview of features to look for and pitfalls to avoid.

Choosing a security key depends on your use case, and I gave a list of these features. Think of which ones make sense to you and the choice will become easier.

Appendix

Here are some technical details about the security keys I own. For each security key, I will include as much information as possible, including the output of the CTAP authenticatorGetInfo command.

Click on a security below to show more details.

Security Key C NFC by Yubico (2023) This one was purchased in late 2023 for $29 off the official Yubico website. It's the newer model which is now black. It previously was blue. It's running firmware version 5.4.3.
$ ctapcli info
Get the Authenticator infomation.

Get all data.
- versions                           = ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"]
- extensions                         = ["credProtect", "hmac-secret"]
- aaguid(16)                         = A4E9FC6D4CBE4758B8BA37598BB5BBAA
- options                            = [("rk", true), ("up", true), ("plat", false), ("clientPin", true), ("credentialMgmtPreview", true)]
- max_msg_size                       = 1200
- pin_uv_auth_protocols              = [2, 1]
- max_credential_count_in_list       = 8
- max_credential_id_length           = 128
- transports                         = ["nfc", "usb"]
- algorithms                         = [("alg", "-7"), ("type", "public-key"), ("alg", "-8"), ("type", "public-key")]
- max_serialized_large_blob_array    = 0
- force_pin_change                   = false
- min_pin_length                     = 4
- firmware_version                   = 328707
- max_cred_blob_length               = 0
- max_rpids_for_set_min_pin_length   = 0
- preferred_platform_uv_attempts     = 0
- uv_modality                        = 0
- remaining_discoverable_credentials = 0

Error: Invalid item
$ fido2-token -I /dev/hidraw0 
proto: 0x02
major: 0x05
minor: 0x04
build: 0x03
caps: 0x05 (wink, cbor, msg)
version strings: U2F_V2, FIDO_2_0, FIDO_2_1_PRE
extension strings: credProtect, hmac-secret
transport strings: nfc, usb
algorithms: es256 (public-key), eddsa (public-key)
aaguid: a4e9fc6d4cbe4758b8ba37598bb5bbaa
options: rk, up, noplat, clientPin, credentialMgmtPreview
fwversion: 0x50403
maxmsgsiz: 1200
maxcredcntlst: 8
maxcredlen: 128
maxlargeblob: 0
minpinlen: 4
pin protocols: 2, 1
pin retries: 8
pin change required: false
uv retries: undefined
$ ctapcli cred
PIN: 

Enumerate discoverable credentials.
- existing discoverable credentials: 1/24
- rp: (id: github.com, name: )
  - credential: (id: ***, name: ***, display_name: ***)
Security Key NFC by Yubico (2019) This is the blue security key. I bought this one from the official Yubico store in February 2019 for $20, plus shipping fee, for a total of $25. It came with firmware version 5.1.2. It doesn't support credential management. Note that Yubico's firmware is proprietary and firmware upgrades are NOT possible. The only way to get a newer firmware is to purchase a new security key.
$ ykman info
Device type: Security Key NFC
Firmware version: 5.1.2
Form factor: Keychain (USB-A)
Enabled USB interfaces: FIDO
NFC transport is enabled.

Applications    USB             NFC          
OTP             Not available   Not available
FIDO U2F        Enabled         Enabled
FIDO2           Enabled         Enabled
OATH            Not available   Not available
PIV             Not available   Not available
OpenPGP         Not available   Not available
YubiHSM Auth    Not available   Not available
$ ctapcli info
Get the Authenticator infomation.

Get all data.
- versions                           = ["U2F_V2", "FIDO_2_0"]
- extensions                         = ["hmac-secret"]
- aaguid(16)                         = 6D44BA9BF6EC2E49B9300C8FE920CB73
- options                            = [("rk", true), ("up", true), ("plat", false), ("clientPin", true)]
- max_msg_size                       = 1200
- pin_uv_auth_protocols              = [1]
- max_credential_count_in_list       = 0
- max_credential_id_length           = 0
- transports                         = []
- algorithms                         = []
- max_serialized_large_blob_array    = 0
- force_pin_change                   = false
- min_pin_length                     = 0
- firmware_version                   = 0
- max_cred_blob_length               = 0
- max_rpids_for_set_min_pin_length   = 0
- preferred_platform_uv_attempts     = 0
- uv_modality                        = 0
- remaining_discoverable_credentials = 0

Error: Invalid item
$ fido2-token -I /dev/hidraw0 
proto: 0x02
major: 0x05
minor: 0x01
build: 0x02
caps: 0x05 (wink, cbor, msg)
version strings: U2F_V2, FIDO_2_0
extension strings: hmac-secret
aaguid: 6d44ba9bf6ec2e49b9300c8fe920cb73
options: rk, up, noplat, clientPin
fwversion: 0x0
maxmsgsiz: 1200
maxcredcntlst: 0
maxcredlen: 0
maxlargeblob: 0
pin protocols: 1
pin retries: 8
pin change required: false
uv retries: undefined
Goldengate G320 by eWBM Note that the G320 is the USB type-C model and the G310 is the same but with a USB type-A connector. I was contacted by eWBM and given these two models (G310 and G320) for free in October 2019 for an evaluation of their products. The maximum number of fingerprints that can be stored is 3.
$ ctapcli info
Get the Authenticator infomation.

Get all data.
- versions                           = ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"]
- extensions                         = ["credProtect", "hmac-secret"]
- aaguid(16)                         = 87DBC5A14C944DC88A4797D800FD1F3C
- options                            = [("rk", true), ("up", true), ("uv", true), ("plat", false), ("clientPin", true), ("credentialMgmtPreview", true), ("userVerificationMgmtPreview", true)]
- max_msg_size                       = 2048
- pin_uv_auth_protocols              = [1]
- max_credential_count_in_list       = 6
- max_credential_id_length           = 192
- transports                         = ["usb"]
- algorithms                         = []
- max_serialized_large_blob_array    = 0
- force_pin_change                   = false
- min_pin_length                     = 0
- firmware_version                   = 0
- max_cred_blob_length               = 0
- max_rpids_for_set_min_pin_length   = 0
- preferred_platform_uv_attempts     = 0
- uv_modality                        = 0
- remaining_discoverable_credentials = 0

Error: Invalid item
$ fido2-token -I /dev/hidraw0 
proto: 0x02
major: 0x02
minor: 0x09
build: 0x65
caps: 0x0d (wink, cbor, nomsg)
version strings: U2F_V2, FIDO_2_0, FIDO_2_1_PRE
extension strings: credProtect, hmac-secret
transport strings: usb
aaguid: 87dbc5a14c944dc88a4797d800fd1f3c
options: rk, up, uv, noplat, clientPin, credentialMgmtPreview, userVerificationMgmtPreview
fwversion: 0x0
maxmsgsiz: 2048
maxcredcntlst: 6
maxcredlen: 192
maxlargeblob: 0
pin protocols: 1
pin retries: 8
pin change required: false
uv retries: undefined
sensor type: 1 (touch)
max samples: 5
$ ctapcli cred
PIN: 

Enumerate discoverable credentials.
- existing discoverable credentials: 0/100

No discoverable credentials.
Titan Security Key v2 by Google (2023) This is the USB type-C model, also known as model K52T. I purchased this model from the official Google Store in November 2023 for $35. It doesn't support credential management!
$ ctapcli info
Get the Authenticator infomation.

Get all data.
- versions                           = ["FIDO_2_0", "U2F_V2"]
- extensions                         = ["credProtect", "hmac-secret"]
- aaguid(16)                         = 42B4FB4A286643B29BF76C6669C2E5D3
- options                            = [("rk", true), ("clientPin", true)]
- max_msg_size                       = 2200
- pin_uv_auth_protocols              = [1]
- max_credential_count_in_list       = 0
- max_credential_id_length           = 0
- transports                         = []
- algorithms                         = []
- max_serialized_large_blob_array    = 0
- force_pin_change                   = false
- min_pin_length                     = 0
- firmware_version                   = 0
- max_cred_blob_length               = 0
- max_rpids_for_set_min_pin_length   = 0
- preferred_platform_uv_attempts     = 0
- uv_modality                        = 0
- remaining_discoverable_credentials = 0

Error: Invalid item
$ fido2-token -I /dev/hidraw0 
proto: 0x02
major: 0x02
minor: 0x00
build: 0x03
caps: 0x05 (wink, cbor, msg)
version strings: FIDO_2_0, U2F_V2
extension strings: credProtect, hmac-secret
aaguid: 42b4fb4a286643b29bf76c6669c2e5d3
options: rk, clientPin
fwversion: 0x0
maxmsgsiz: 2200
maxcredcntlst: 0
maxcredlen: 0
maxlargeblob: 0
pin protocols: 1
pin retries: 8
pin change required: false
uv retries: undefined
Solo 1 Hacker by SoloKeys I bought this key in February 2019 from the official SoloKeys website for $20, plus shipping fee, for a total of $30. This is an old model. The newer one is the Solo 2. The firmware was later upgraded to version `4.1.5 unlocked`.
$ solo key version
4.1.5 unlocked
$ ctapcli info
Get the Authenticator infomation.

Get all data.
- versions                           = ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"]
- extensions                         = ["credProtect", "hmac-secret"]
- aaguid(16)                         = 8876631BD4A0427F57730EC71C9E0279
- options                            = [("rk", true), ("up", true), ("plat", false), ("credMgmt", true), ("clientPin", true)]
- max_msg_size                       = 1200
- pin_uv_auth_protocols              = [1]
- max_credential_count_in_list       = 20
- max_credential_id_length           = 128
- transports                         = []
- algorithms                         = []
- max_serialized_large_blob_array    = 0
- force_pin_change                   = false
- min_pin_length                     = 0
- firmware_version                   = 0
- max_cred_blob_length               = 0
- max_rpids_for_set_min_pin_length   = 0
- preferred_platform_uv_attempts     = 0
- uv_modality                        = 0
- remaining_discoverable_credentials = 0

Error: Invalid item
$ fido2-token -I /dev/hidraw0 
proto: 0x02
major: 0x00
minor: 0x00
build: 0x00
caps: 0x05 (wink, cbor, msg)
version strings: U2F_V2, FIDO_2_0, FIDO_2_1_PRE
extension strings: credProtect, hmac-secret
aaguid: 8876631bd4a0427f57730ec71c9e0279
options: rk, up, noplat, credMgmt, clientPin
fwversion: 0x0
maxmsgsiz: 1200
maxcredcntlst: 20
maxcredlen: 128
maxlargeblob: 0
pin protocols: 1
pin retries: 8
pin change required: false
uv retries: undefined
$ ctapcli cred
PIN: 

Enumerate discoverable credentials.
- existing discoverable credentials: 1/49
- rp: (id: webauthn.io, name: test)
  - credential: (id: ***, name: test, display_name: test)