Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added custom flags support #15

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions soft_webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
from base64 import urlsafe_b64encode
from enum import Enum
from struct import pack

from cryptography.hazmat.backends import default_backend
Expand All @@ -17,6 +18,22 @@
from fido2.utils import sha256


# https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
class AuthenticatorDataFlags(Enum):
"""
Values for authenticator data flags
"""

USER_PRESENT = (1 << 0)
RESERVED1 = (1 << 1)
USER_VERIFIED = (1 << 2)
BACKUP_ELIGIBLE = (1 << 3)
BACKED_UP = (1 << 4)
RESERVED2 = (1 << 5)
ATTESTED_CREDENTIAL_DATA_INCLUDED = (1 << 6)
EXTENSION_DATA_INCLUDED = (1 << 7)


class SoftWebauthnDevice():
"""
This simulates the Webauthn browser API with a authenticator device
Expand All @@ -27,11 +44,33 @@ class SoftWebauthnDevice():
def __init__(self):
self.credential_id = None
self.private_key = None
self.aaguid = b'\x00'*16
self.aaguid = b'\x00' * 16
self.rp_id = None
self.user_handle = None
self.sign_count = 0

@staticmethod
def convert_flags(flags):
"""Converts flag-like values into final binary representation"""

result = 0
for flag in flags:
if isinstance(flag, AuthenticatorDataFlags):
value = flag.value
elif isinstance(flag, int):
if flag > (1 << 7):
raise ValueError(f"Invalid flag value {flag}")
value = flag
else:
raise ValueError(
f"Flag can either be an integer or an instance of AuthenticatorDataFlags. "
f"{flag} was provided, which is {type(flag)}"
)

result |= value

return result.to_bytes(1, "little")

def cred_init(self, rp_id, user_handle):
"""initialize credential for rp_id under user_handle"""

Expand All @@ -48,9 +87,14 @@ def cred_as_attested(self):
self.credential_id,
ES256.from_cryptography_key(self.private_key.public_key()))

def create(self, options, origin):
def create(self, options, origin, flags=None):
"""create credential and return PublicKeyCredential object aka attestation"""

if flags is None:
flags = [AuthenticatorDataFlags.ATTESTED_CREDENTIAL_DATA_INCLUDED, AuthenticatorDataFlags.USER_PRESENT]

flags = self.convert_flags(flags)

if {'alg': -7, 'type': 'public-key'} not in options['publicKey']['pubKeyCredParams']:
raise ValueError('Requested pubKeyCredParams does not contain supported type')

Expand All @@ -68,7 +112,6 @@ def create(self, options, origin):
}

rp_id_hash = sha256(self.rp_id.encode('ascii'))
flags = b'\x41' # attested_data + user_present
sign_count = pack('>I', self.sign_count)
credential_id_length = pack('>H', len(self.credential_id))
cose_key = cbor.encode(ES256.from_cryptography_key(self.private_key.public_key()))
Expand All @@ -90,9 +133,14 @@ def create(self, options, origin):
'type': 'public-key'
}

def get(self, options, origin):
def get(self, options, origin, flags=None):
"""get authentication credential aka assertion"""

if flags is None:
flags = [AuthenticatorDataFlags.USER_PRESENT]

flags = self.convert_flags(flags)

if self.rp_id != options['publicKey']['rpId']:
raise ValueError('Requested rpID does not match current credential')

Expand All @@ -107,7 +155,6 @@ def get(self, options, origin):
client_data_hash = sha256(client_data)

rp_id_hash = sha256(self.rp_id.encode('ascii'))
flags = b'\x01'
sign_count = pack('>I', self.sign_count)
authenticator_data = rp_id_hash + flags + sign_count

Expand Down
51 changes: 51 additions & 0 deletions tests/test_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""SoftWebauthnDevice tests for AuthenticatorDataFlags"""

import pytest
from soft_webauthn import AuthenticatorDataFlags, SoftWebauthnDevice


def test_valid_enum():
"""Tests flags with only enums"""

assert SoftWebauthnDevice.convert_flags([
AuthenticatorDataFlags.USER_PRESENT,
AuthenticatorDataFlags.USER_VERIFIED
]) == (0b00000101).to_bytes(1, "little")


def test_valid_int():
"""Tests flags with only ints"""

assert SoftWebauthnDevice.convert_flags([
(1 << 0),
(1 << 2)
]) == (0b00000101).to_bytes(1, "little")


def test_valid_mixed():
"""Tests flags with both enums and ints"""

assert SoftWebauthnDevice.convert_flags([
AuthenticatorDataFlags.USER_PRESENT,
(1 << 2)
]) == (0b00000101).to_bytes(1, "little")


def test_invalid_instance():
"""Tests if an error is raised if a flag is not the correct type"""

with pytest.raises(ValueError):
SoftWebauthnDevice.convert_flags([
"something",
AuthenticatorDataFlags.USER_PRESENT
])


def test_out_of_range():
"""Tests if an error is raised if a flag is out of range"""

with pytest.raises(ValueError):
SoftWebauthnDevice.convert_flags([
(1 << 0),
(1 << 8)
])
Loading