import re, json, hashlib import cbor2 from .attestation import FIDOU2FAttestationStatement from .authenticators import AuthenticatorData from .cose import Algorithms from .util import b64_encode, b64url_decode from .util.compat import token_bytes class RelyingPartyManager: def __init__(self, rp_name, rp_id=None, credential_storage_backend=None): self.storage_backend = credential_storage_backend self.rp_name = rp_name self.rp_id = rp_id def get_registration_options(self, email, display_name=None, icon=None): "Get challenge parameters that will be passed to the user agent's navigator.credentials.get() method" challenge = token_bytes(32) options = { "challenge": b64_encode(challenge), "rp": { "name": self.rp_name, "id": self.rp_id, }, "user": { "id": b64_encode(email.encode()), "name": email, "displayName": display_name if display_name else email, "icon": icon, }, "pubKeyCredParams": [ {"type": "public-key", "alg": Algorithms.ES256}, {"type": "public-key", "alg": Algorithms.ES384}, {"type": "public-key", "alg": Algorithms.ES512}, ], "timeout": 60 * 1000, "excludeCredentials": [], "attestation": "direct", "extensions": {"loc": True} } self.storage_backend.save_challenge_for_user(email=email, challenge=challenge, type="registration") return options def get_authentication_options(self, email): credential = self.storage_backend.get_credential_by_email(email) challenge = token_bytes(32) options = { "challenge": challenge, "timeout": 60 * 1000, "allowCredentials": [ {"type": "public-key", "id": b64_encode(credential.id)} ], } self.storage_backend.save_challenge_for_user(email=email, challenge=challenge, type="authentication") return options # https://www.w3.org/TR/webauthn/#registering-a-new-credential def register(self, client_data_json, attestation_object, email): "Store the credential public key and related metadata on the server using the associated storage backend" authenticator_attestation_response = cbor2.loads(attestation_object) email = email.decode() if not re.match(r"[^@]+@[^@]+\.[^@]+", email): raise Exception("Invalid email address") client_data_hash = hashlib.sha256(client_data_json).digest() client_data = json.loads(client_data_json) assert client_data["type"] == "webauthn.create" print("client data", client_data) expect_challenge = self.storage_backend.get_challenge_for_user(email=email, type="registration") assert b64url_decode(client_data["challenge"]) == expect_challenge print("expect RP ID:", self.rp_id) if self.rp_id: assert "https://" + self.rp_id == client_data["origin"] # Verify that the value of C.origin matches the Relying Party's origin. # Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP. authenticator_data = AuthenticatorData(authenticator_attestation_response["authData"]) assert authenticator_data.user_present # If user verification is required for this registration, # verify that the User Verified bit of the flags in authData is set. assert authenticator_attestation_response["fmt"] == "fido-u2f" att_stmt = FIDOU2FAttestationStatement(authenticator_attestation_response['attStmt']) attestation = att_stmt.validate(authenticator_data, rp_id_hash=authenticator_data.rp_id_hash, client_data_hash=client_data_hash) credential = attestation.credential # TODO: ascertain user identity here self.storage_backend.save_credential_for_user(email=email, credential=credential) return {"registered": True} # https://www.w3.org/TR/webauthn/#verifying-assertion def verify(self, authenticator_data, client_data_json, signature, user_handle, raw_id, email): "Ascertain the validity of credentials supplied by the client user agent via navigator.credentials.get()" email = email.decode() if not re.match(r"[^@]+@[^@]+\.[^@]+", email): raise Exception("Invalid email address") client_data_hash = hashlib.sha256(client_data_json).digest() client_data = json.loads(client_data_json) assert client_data["type"] == "webauthn.get" expect_challenge = self.storage_backend.get_challenge_for_user(email=email, type="authentication") assert b64url_decode(client_data["challenge"]) == expect_challenge print("expect RP ID:", self.rp_id) if self.rp_id: assert "https://" + self.rp_id == client_data["origin"] # Verify that the value of C.origin matches the Relying Party's origin. # Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP. authenticator_data = AuthenticatorData(authenticator_data) assert authenticator_data.user_present credential = self.storage_backend.get_credential_by_email(email) credential.verify(signature, authenticator_data.raw_auth_data + client_data_hash) # signature counter check return {"verified": True}