FIDO2 (Fast Identity Online 2.0) is a passwordless authentication standard that allows users to log in securely using hardware security keys such as YubiKey.
This system is based on Public Key Cryptography, ensuring the security and integrity of the authentication process.
This report provides a detailed explanation of the FIDO2 architecture, authentication process, and the key data structures used.
The FIDO2 architecture consists of the following key components:
- Client: The user-side application that interacts with the server and performs authentication via YubiKey.
- Authenticator: The security device (e.g., YubiKey) that generates keys, processes signing requests, and authenticates the user.
- Server: Stores user Credential IDs and Public Keys, and verifies authentication requests.
- Relying Party (RP): The application or website that requests authentication from the Client.
The FIDO2 authentication process consists of two main stages: Registration and Authentication.
- Client sends a registration request to the server.
- Server generates a Challenge (random challenge string) and sends it back to the Client.
- Client uses YubiKey to generate a Key Pair.
- YubiKey generates a unique Credential ID and signs the Challenge using the Private Key.
- Client sends the Credential ID and Public Key to the Server.
- Server stores the Credential ID and Public Key, completing the registration.
- Client sends an authentication request to the Server.
- Server generates a new Challenge and returns the Credential ID to the Client.
- Client retrieves the Credential ID and signs the Challenge using the corresponding Private Key.
- Client sends
signed_data,authenticator_data, andcredential_idto the Server. - Server retrieves the stored Public Key and verifies
signed_data. - If signature verification succeeds, authentication is successful.
- Purpose: Identifies a specific credential, allowing the server to retrieve the correct Public Key.
- Characteristics:
- Generated by YubiKey, uniquely associated with a specific website.
- Unique and unpredictable, ensuring security.
- Example:
986f86c2c61f8f78df847ea84cce8bf58204e319faffcc828263a78e4b27c6d42f4b1059b345d0380ce7a841889b4233a91bd11b5bac229daf77fc3a31e17871
- Purpose: Signed by YubiKey using the Private Key to authenticate the challenge.
- Verification Process:
- The Server retrieves the Public Key using the Credential ID.
- The Server verifies
signed_datausing the Public Key. - If the signature is valid, it confirms ownership of the Private Key.
- Example:
3044022047a0b765665df498ffe0132d07163796fd2b7ea6815054eb0065d93939ed8396 02200719112b90ce5868df925bffe4491a20ae9994e517d5b4af394b39d8b4081928
- Purpose: Provides additional authentication information, ensuring the request has not been tampered with.
- Contents:
- Relying Party Hash (site identifier)
- Flags (e.g., user presence detection, attestation status)
- Signature Counter (prevents replay attacks)
- Example:
49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000009
- Purpose: Prevents replay attacks. The server generates a new challenge for each authentication request.
- Characteristics:
- Different for every authentication request.
- Must be signed by the YubiKey.
- Example (COSE Key - CBOR Object Signing and Encryption format):
pQECAzgYIAEhWCDdNfh6giBH2b+1VjQ2UEJBycQmze0jCcKeHl30b10InyJYINnc+d7qECqsoVRv5XC4lMSjddl2NIDoTKYAAcKfQI3/
import base64
import cbor2
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
# Decode COSE Key (Public Key)
public_key_cose_b64 = "pQECAzgYIAEhWCDdNfh6giBH2b+1VjQ2UEJBycQmze0jCcKeHl30b10InyJYINnc+d7qECqsoVRv5XC4lMSjddl2NIDoTKYAAcKfQI3/"
public_key_cose = base64.b64decode(public_key_cose_b64)
cose_key = cbor2.loads(public_key_cose)
x_coordinate = int.from_bytes(cose_key[-2], "big")
y_coordinate = int.from_bytes(cose_key[-3], "big")
public_numbers = ec.EllipticCurvePublicNumbers(x_coordinate, y_coordinate, ec.SECP256R1())
public_key = public_numbers.public_key()
# Decode signed data
signed_data_hex = "3044022047a0b765665df498ffe0132d07163796fd2b7ea6815054eb0065d93939ed839602200719112b90ce5868df925bffe4491a20ae9994e517d5b4af394b39d8b4081928"
signed_data = bytes.fromhex(signed_data_hex)
# Verify signature
try:
public_key.verify(signed_data, b"challenge_data", ec.ECDSA(hashes.SHA256()))
print("✅ Signature verification successful!")
except:
print("❌ Signature verification failed!")