-
Notifications
You must be signed in to change notification settings - Fork 1
Description
The major security considerations
- signature verification
- challenge
- signCount
- userHandle
- Credential IDs
- Relying Party (domain)
- etc
Signature Verification
You must verify signatures. You cannot rely on the challenge alone.
- you must store the PublicKey by Credential ID (1) on Registration (2)
- (1) a.k.a.
.id,.rawId,response.authenticator.attestedCredential.credentialId - (2) a.k.a. device attestation,
webauthn.create
- (1) a.k.a.
- you must retrieve the PublicKey by Credential ID or Challenge on Authentication (3)
- (3) a.k.a. attestation,
webauthn.get
- (3) a.k.a. attestation,
- the WebAuthn (COSE) signature is in ASN.1 format, but WebCrypto requires it as p1363
(i.e. if you use WebCrypto in Node or Bun or Deno) attToBeSignedis the concatenation ofauthDataand the sha256 hash ofclientData
challenge
- you must generate the challenge server side
otherwise an attacker with device access could generate many signed challenges and use them in the future - DO NOT verify the signature alone, you MUST VERIFY the challenge
otherwise an attacker who gains access to the device can possibly create lots and lots of authentications and use them on future dates.
Sign Count
Sign count mitigates hardware cloning (e.g. YubiKey) and out-of-sync / offline / system-time-faked / cloned cloud authenticators (e.g. Apple iCloud Keychain).
if signCount > 0 then you should track the sign count and make sure that each new sign count is ALWAYS greater than the previous one (savedCount = newCount; db.save();
Only save the new signCount AFTER successful signature verification - otherwise an attacker can present a very high signCount, have it saved, and then lock the user out of their account.
userHandle
Don't send userHandle to the server. It's a misnomer. It's actually arbitrary user data. It can be used to store a local encryption key, for example.
Keep it secret. Keep it safe. Keep it client-side.
Note: in at least one place it's referred to as "id". This is incorrect. The "id" is the "credential id" and the remote user id is "user.name".
See also: mylofi/local-data-lock#7
Credential IDs
bottom line: rate limit the retrieval of credential ids by ip address or other means so that an attacker can't just scan email addresses, and don't include any metadata such as created_at or user agent, etc with the credential ids you return
These are more used to identify the Authenticator than the specific Credential.
If you fail to provide these on new Passkey registration, you could easily permanently delete and replace an existing Passkey (i.e. iCloud only allows one Passkey per domain and will delete rather than add).
Since they are required before the user is authenticated in order to authenticate the user, they are essentially public values.
However, they also leak a small amount of statistical information about the user - the format of the ID may fingerprint which device or service is being used (by length, character distribution or prefix or suffix). Also, they show how many authenticators a user has.
So there's a bit of a catch 22:
- asking the user to try to authenticate and then register on failure is cumbersome and annoying
- if the user has cleared out their browser application cache or is on a different computer, you don't know if they have an authenticator (i.e. "silent" authentication doesn't actually work in practice)
- if you request the credential ids by email address, there's a small amount of info that an attacker can farm by looking up these ids in your system by email or whatever - and you obviously can't mask them otherwise you can't use them
Relying Party
In theory you should check that the domain or domain hash matches (rp.id / rpid / relying party / rpidhash) matches, but in practice, this doesn't actually matter because only a malicious device or browser plugin would be sharing your private key between accounts internally, at which point you've got other problems.
Authenticator devices or services or plugins that you can get from legitimate vendors (Apple, Yubico, Microsoft, Android, Lastpass, etc), all create pairwise Credential IDs - meaning that the ID has a unique constraint across domain and user id user accounts - so you're not going to get an "attestation" that's signed with the wrong domain.
The theoretical vulnerability requires that the device or service that uses a single Public Key and a single Credential ID to sign attestations for many domains, and that the user has given such as signed attestation to a malicious domain which then relays to your domain.
Also, the cross-origin spec, as of today (Oct 2024), does not exist. It's mentioned that there may be one in the future, and there's a cross-origin flag reserved for that use, but it's not spec'd or implemented.
etc
WebAuthn is chuck full of YAGNI.
There are so many different representations of similar data, so many things that will obviously never actually get implemented, and actual implementations lacking many things that we wish they would have (i.e. seamless background login, JSON).
Don't dig too deep. As Douglas Crockford warns: almost anything can be useful - just look at Rube Goldberg machines - but that doesn't mean it's a good idea.
Stick to the core things you need. Ignore the rest until the use case arises.
You could lose your life trying to build a framework that finds utility for every possible nook and cranny of the spec.