88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
"""WebAuthn passkey ceremony helpers for the single dashboard operator."""
|
|
|
|
import base64
|
|
import json
|
|
import secrets
|
|
|
|
from webauthn import (
|
|
generate_authentication_options,
|
|
generate_registration_options,
|
|
options_to_json,
|
|
verify_authentication_response,
|
|
verify_registration_response,
|
|
)
|
|
from webauthn.helpers.structs import (
|
|
AuthenticatorSelectionCriteria,
|
|
PublicKeyCredentialDescriptor,
|
|
ResidentKeyRequirement,
|
|
UserVerificationRequirement,
|
|
)
|
|
|
|
|
|
def encode(value: bytes) -> str:
|
|
return base64.urlsafe_b64encode(value).rstrip(b"=").decode()
|
|
|
|
|
|
def decode(value: str) -> bytes:
|
|
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
|
|
|
|
def registration_options(*, rp_id: str, excluded: list[bytes] | None = None) -> tuple[dict, bytes]:
|
|
challenge = secrets.token_bytes(32)
|
|
options = generate_registration_options(
|
|
rp_id=rp_id,
|
|
rp_name="Stackchain Dashboard",
|
|
user_name="stackchain-operator",
|
|
user_display_name="Stackchain operator",
|
|
challenge=challenge,
|
|
exclude_credentials=[
|
|
PublicKeyCredentialDescriptor(id=credential_id)
|
|
for credential_id in (excluded or [])
|
|
],
|
|
authenticator_selection=AuthenticatorSelectionCriteria(
|
|
resident_key=ResidentKeyRequirement.PREFERRED,
|
|
user_verification=UserVerificationRequirement.REQUIRED,
|
|
),
|
|
)
|
|
return json.loads(options_to_json(options)), challenge
|
|
|
|
|
|
def authentication_options(*, rp_id: str, credentials: list[bytes]) -> tuple[dict, bytes]:
|
|
challenge = secrets.token_bytes(32)
|
|
options = generate_authentication_options(
|
|
rp_id=rp_id,
|
|
challenge=challenge,
|
|
allow_credentials=[
|
|
PublicKeyCredentialDescriptor(id=credential_id)
|
|
for credential_id in credentials
|
|
],
|
|
user_verification=UserVerificationRequirement.REQUIRED,
|
|
)
|
|
return json.loads(options_to_json(options)), challenge
|
|
|
|
|
|
def verify_registration(
|
|
*, credential: dict, challenge: bytes, rp_id: str, origin: str
|
|
):
|
|
return verify_registration_response(
|
|
credential=credential,
|
|
expected_challenge=challenge,
|
|
expected_rp_id=rp_id,
|
|
expected_origin=origin,
|
|
require_user_verification=True,
|
|
)
|
|
|
|
|
|
def verify_authentication(
|
|
*, credential: dict, challenge: bytes, rp_id: str, origin: str, stored
|
|
):
|
|
return verify_authentication_response(
|
|
credential=credential,
|
|
expected_challenge=challenge,
|
|
expected_rp_id=rp_id,
|
|
expected_origin=origin,
|
|
credential_public_key=stored.public_key,
|
|
credential_current_sign_count=stored.sign_count,
|
|
require_user_verification=True,
|
|
)
|