The article provides a technical overview of the procedures and considerations for verifying WebAuthn/FIDO2 assertion responses on a server, emphasizing the importance of this protocol in enhancing security and preventing phishing attacks.
Abstract
The article delves into the specifics of server-side validation for WebAuthn/FIDO2 assertion responses, which are critical for ensuring the security of user authentication. It outlines the general structure of FIDO2 responses, including the importance of the clientDataJSON field, which contains session information. The author guides readers through decoding this JSON structure and explains the significance of the challenge, type, and origin fields. The article also distinguishes between attestation responses, used for registering credentials, and assertion responses, used for authenticating credentials. It provides detailed insights into parsing attestation objects, verifying different attestation formats, and handling cases where no attestation is provided. Additionally, the author discusses user information extraction, the verification process for both attestation and assertion responses, and the steps to follow when encountering discrepancies that may indicate phishing or replay attacks. The article concludes with a list of the author's other relevant works and licensing information for commercial use or translation.
Opinions
The author emphasizes the severity of security breaches, using real-world scenarios to illustrate the potential consequences of inadequate authentication measures.
There is a clear endorsement of FIDO2 as a solution to eliminate phishing, expressing confidence in its effectiveness.
The article suggests that some browsers may include additional information beyond the standard fields, implying a potential for variability in implementation.
The author provides a nuanced view on attestation formats, including deprecated formats like Android SafetyNet and Apple Anonymous Attestation, indicating a preference for more secure and updated methods.
By providing specific examples and code snippets, the author demonstrates a hands-on approach to implementing FIDO2 server-side verification, catering to a technical audience.
The inclusion of updates and corrections to the article reflects a commitment to accuracy and a responsive approach to reader feedback.
The licensing terms provided at the end of the article indicate the author's interest in sharing knowledge while retaining control over commercial use and derivatives.
WebAuthn/FIDO2: Verifying assertion responses
In this article we will talk about procedures that server will need to perform in order to validate WebAuthn response. If you are interested in playing with WebAuthn first, you should read my article: “Introduction to WebAuthn API”
“Your employees are locked out of their laptops. Their data is gone. They don’t even know what meetings they have today and will be trying to recover for months.” (Sony Pictures)
“Your customer data has been released, including their credit card numbers and social security numbers. The data has been insidiously pilfered for months. Your company is the top headline of every news site and your shareholders are not happy. You watch as you stock price dives, knowing that this is only the beginning.”
Now lets’s wake up, have cuppa tea or coffee, and start building FIDO2 server so nothing like this would ever happen, because FIDO2 is here to kill phishing. Once and for all.
General FIDO2 response structure looks like this:
rawId and id — is credential identifier on the device
response — contains attestation or assertion data. I will get into specifics later.
response.clientDataJSON — base64url encoded buffer of the JSON structure that contains session information. That’s the only response field I will describe for now.
type — type of credential. Must be set to “public-key”
Lets explore clientDataJSON. You can decode it on the server side using base64url lib in Node.js:
Decoding ClientDataJSON reveals all the important information about the session:
challenge —the challenge that was sent to authr
type — type of the call. If you were creating credential(registering authr), then you will get type “webauthn.create”. If you were getting assertion(authenticating) you will get webauthn.get.
origin — the origin of the website. To those who are not familiar with URLs, the origin is basically protocol, host and port. So if your are calling WebAuthn API while your use is located at “https://example.com/login”, then origin will be “https://example.com”. If he calls from “http://localhost:2823/test” then the origin will be “http://localhost:2823”. Please note that WebAuthn API will not work on pages loaded over HTTP, unless it is localhost, which is considered secure context.
All other fields can be ignored. Some browsers will put there some additional useful information, such as in current example Chrome team decided to let users know that they should not simply do template check. Please don’t.
Getting back to “response” field. Responses can be of two types:
1. Attestation response — for registering credential
2. Assertion response — for authenticating credential
Attestation
Example of attestation response we’ve seen earlier:
The attestationObject contains base64url encoded buffer of CBOR encoded attestation object. You can parse it using base64url and cbor libs in Node.js:
When parsed you will get this structure:
fmt — attestation format. It can be “packed”, “fido-u2f”, “none”, “android-key”, “android-safetynet”, “tpm” and “apple”.
authData — a raw buffer struct containing user info.
attStmt — attestation statement data. The structure of the statement and the procedures to verify it are depending on the type of the format that is defined by “fmt”.
Now, I won’t be talking about verifying each attestation here. Instead I have written, and still writing, blog posts about verifying each format:
One of the attestation formats called “none”. When you getting it, that means two things:
1. You really don’t need attestation, and so you are deliberately ignoring it.
2. You forgot to set attestation flag to “direct” when making credential.
If you are getting attestation with “fmt” set to “none”, then no attestation is provided, and you don’t have anything to verify. Simply extract user relevant information as specified below and save it to the database.
User information is stored in authData. AuthData is a rawBuffer struct:
RPIDHash — is the hash of the rpId which is basically the effective domain or host. For example: “https://example.com” effective domain is “example.com”
Flags — 8bit flag that defines the state of the authenticator during the authentication. Bits 0 and 2 are User Presence and User Verification flags. Bit 6 is AT(Attested Credential Data). Must be set when attestedCredentialData is presented. Bit 7 must be set if extension data is presented.
Counter — 4byte counter.
RPIDHash, Flags and Counter is mandatory for both Attestation and Assertion responses. AttestedCredentialData is only for attestation.
AAGUID — authenticator attestation identifier — a unique identifier of authenticator model
CredID — Credential Identifier. The length is defined by credIdLen. Must be the same as id/rawId.
COSEPubKey — COSE encoded public key
Here is a method how to parse authData:
Assertion
The assertion response looks like this:
Instead of attestationObject, now you just have straight forward signature, userHandle, and authenticatorData(same as authData in attestation).
signature — self explanatory
authenticatorData —same as authData, but without attestedCredentialData
userHandle — user.id that was send during credential creation. In U2F this field will always be empty.
Verifying response
For both Attestation and Assertion
Decode ClientDataJSON
Check that challenge is set to the challenge you’ve sent
Check that origin is set to the the origin of your website. If it’s not raise the alarm, and log the event, because someone tried to phish your user
Check that type is set to either “webauthn.create” or “webauthn.get”.
Parse authData or authenticatorData.
Check that flags have UV or UP flags set.
If your RPID set to the origin, hash the domain part of the origin: hash(“example.com”). If you have different RPID, then hash that e.g. hash(“auth.example.com”). Save the hash to the ExpectedRPIDHash
Verify that authData.rpIdHash matches ExpectedRPIDHash