Skip to content

Commit

Permalink
Added custom flags support
Browse files Browse the repository at this point in the history
  • Loading branch information
tomadimitrie committed Aug 28, 2024
1 parent ff1c909 commit 0c3497b
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 5 deletions.
51 changes: 46 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,18 @@
from fido2.utils import sha256


# https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
class AuthenticatorDataFlags(Enum):
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 +40,31 @@ 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):
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 +81,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 +106,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 +127,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 +149,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
41 changes: 41 additions & 0 deletions tests/test_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""SoftWebauthnDevice tests for AuthenticatorDataFlags"""

from soft_webauthn import AuthenticatorDataFlags, SoftWebauthnDevice
import pytest


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


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


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


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


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

0 comments on commit 0c3497b

Please sign in to comment.