From ee8df3c9cec8a61e78a0280e8dd805b11f8c6c19 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 28 Sep 2022 15:16:20 +0200
Subject: [PATCH 01/26] fix: make_tests_run_on_windows

---
 privacyidea/config.py | 31 +++++++++++++++++++++----------
 privacyidea/models.py |  4 ++--
 2 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/privacyidea/config.py b/privacyidea/config.py
index 15015c262..9e388b868 100644
--- a/privacyidea/config.py
+++ b/privacyidea/config.py
@@ -1,14 +1,15 @@
 # -*- coding: utf-8 -*-
 
-import os
 import logging
+import os
 import random
 import string
+from sys import platform
+
 log = logging.getLogger(__name__)
 basedir = os.path.abspath(os.path.dirname(__file__))
 basedir = "/".join(basedir.split("/")[:-1]) + "/"
 
-
 pubtest_key = b"""-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0stF+PVh/qR7I82ywgDW
 X2rPxKnlBugU33SAbxZjfNz+aKYYM+irKJ0IdTOLtQ85DzZaEhocqO7cg1Qw85Ac
@@ -34,7 +35,8 @@ WQIDAQAB
 def _random_password(size):
     log.info("SECRET_KEY not set in config. Generating a random key.")
     passwd = [random.choice(string.ascii_lowercase + \
-                            string.ascii_uppercase + string.digits) for _x in range(size)]
+                            string.ascii_uppercase + string.digits) for _x in
+              range(size)]
     # return shuffled password
     random.shuffle(passwd)
     return "".join(passwd)
@@ -44,7 +46,8 @@ class Config(object):
     SECRET_KEY = os.environ.get('SECRET_KEY')
     # SQL_ALCHEMY_DATABASE_URI = "mysql://privacyidea:XmbSrlqy5d4IS08zjz"
     # "GG5HTt40Cpf5@localhost/privacyidea"
-    PI_ENCFILE = os.path.join(basedir, "tests/testdata/enckey")
+    # PI_ENCFILE = os.path.join(basedir, "tests/testdata/enckey")
+    PI_ENCFILE = os.path.join("tests/testdata/enckey")
     PI_HSM = "default"
     PI_AUDIT_MODULE = "privacyidea.lib.auditmodules.sqlaudit"
     PI_AUDIT_KEY_PRIVATE = os.path.join(basedir, "tests/testdata/private.pem")
@@ -65,7 +68,8 @@ class DevelopmentConfig(Config):
     DEBUG = True
     SECRET_KEY = os.environ.get('SECRET_KEY') or 't0p s3cr3t'
     SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
+                              'sqlite:///' + os.path.join(basedir,
+                                                          'data-dev.sqlite')
     PI_LOGLEVEL = logging.DEBUG
     PI_TRANSLATION_WARNING = "[Missing]"
 
@@ -75,8 +79,14 @@ class TestingConfig(Config):
     # This is used to encrypt the auth token
     SUPERUSER_REALM = ['adminrealm']
     SECRET_KEY = 'secret'
-    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
+    SQLITE_DB_PATH = 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
+    if platform == 'win32':
+        SQLITE_DB_PATH = 'sqlite:///' + os.path.join(basedir, 'Users',
+                                                     os.getlogin(),
+                                                     'Documents',
+                                                     'data-test.sqlite')
+    SQLALCHEMY_DATABASE_URI = os.environ.get(
+        'TEST_DATABASE_URL') or SQLITE_DB_PATH
     # This is used to encrypt the admin passwords
     PI_PEPPER = ""
     # This is only for testing encrypted files
@@ -124,8 +134,9 @@ class AltUIConfig(TestingConfig):
 
 class ProductionConfig(Config):
     SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data.sqlite')
-    #SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
+                              'sqlite:///' + os.path.join(basedir,
+                                                          'data.sqlite')
+    # SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
     # This is used to encrypt the auth_token
     SECRET_KEY = os.environ.get('SECRET_KEY') or _random_password(24)
     # This is used to encrypt the admin passwords
@@ -144,7 +155,7 @@ class HerokuConfig(Config):
                               "wqy_btZE3CPPNWsmkfdmeorxy6@" \
                               "ec2-54-83-0-61.compute-1." \
                               "amazonaws.com:5432/d6fjidokoeilp6"
-    #SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
+    # SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
     # This is used to encrypt the auth_token
     SECRET_KEY = os.environ.get('SECRET_KEY') or 't0p s3cr3t'
     # This is used to encrypt the admin passwords
diff --git a/privacyidea/models.py b/privacyidea/models.py
index 35dda42b3..3e78a7945 100644
--- a/privacyidea/models.py
+++ b/privacyidea/models.py
@@ -107,10 +107,10 @@ def save_config_timestamp(invalidate_config=True):
     """
     c1 = Config.query.filter_by(Key=PRIVACYIDEA_TIMESTAMP).first()
     if c1:
-        c1.Value = datetime.now().strftime("%s")
+        c1.Value = datetime.now().strftime("%S")
     else:
         new_timestamp = Config(PRIVACYIDEA_TIMESTAMP,
-                               datetime.now().strftime("%s"),
+                               datetime.now().strftime("%S"),
                                Description="config timestamp. last changed.")
         db.session.add(new_timestamp)
     if invalidate_config:
-- 
GitLab


From ca690397abd1b7da3ee776bdac8453061543e577 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 28 Sep 2022 15:16:36 +0200
Subject: [PATCH 02/26] feat: backup_code_token

---
 privacyidea/lib/config.py                     |   1 +
 privacyidea/lib/tokens/backupcodetoken.py     | 265 ++++++++++++++++++
 .../token/views/token.enroll.backupcode.html  |   5 +
 .../views/token.enrolled.backupcode.html      |  58 ++++
 tests/test_lib_tokens_backupcode.py           | 150 ++++++++++
 5 files changed, 479 insertions(+)
 create mode 100644 privacyidea/lib/tokens/backupcodetoken.py
 create mode 100644 privacyidea/static/components/token/views/token.enroll.backupcode.html
 create mode 100644 privacyidea/static/components/token/views/token.enrolled.backupcode.html
 create mode 100644 tests/test_lib_tokens_backupcode.py

diff --git a/privacyidea/lib/config.py b/privacyidea/lib/config.py
index 819100544..10a948078 100644
--- a/privacyidea/lib/config.py
+++ b/privacyidea/lib/config.py
@@ -699,6 +699,7 @@ def get_token_list():
     """
     module_list = set()
 
+    module_list.add("privacyidea.lib.tokens.backupcodetoken")
     module_list.add("privacyidea.lib.tokens.daplugtoken")
     module_list.add("privacyidea.lib.tokens.hotptoken")
     module_list.add("privacyidea.lib.tokens.motptoken")
diff --git a/privacyidea/lib/tokens/backupcodetoken.py b/privacyidea/lib/tokens/backupcodetoken.py
new file mode 100644
index 000000000..7d64dfbf7
--- /dev/null
+++ b/privacyidea/lib/tokens/backupcodetoken.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+#
+#  2022-09-22 Peter Bolha <485456@mail.muni.cz>
+#             BUC token with to be randomly used BUC values
+#
+#  (c) 2022 Peter Bolha - 485456@mail.muni.cz
+#
+# This code is free software; you can redistribute it and/or
+# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or any later version.
+#
+# This code is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+This file contains the definition of the backupcode token class
+It depends on the DB model, and the lib.tokenclass.
+"""
+
+import ast
+import logging
+import secrets
+import string
+
+from privacyidea.lib import _
+from privacyidea.lib.decorators import check_token_locked
+from privacyidea.lib.log import log_with
+from privacyidea.lib.policy import ACTION, GROUP, SCOPE
+from privacyidea.lib.tokenclass import TokenClass
+
+log = logging.getLogger(__name__)
+
+# number of generated tokens per method call
+DEFAULT_COUNT = 10
+# length of each token
+DEFAULT_LENGTH = 16
+CHARACTER_POOL = list(string.digits) + list(string.ascii_letters)
+SECRET_GENERATOR = secrets.SystemRandom()
+
+
+class BACKUPCODEACTION(object):
+    BACKUPCODETOKEN_COUNT = "backupcodetoken_count"
+    BACKUPCODETOKEN_LENGTH = "backupcodetoken_length"
+
+
+class BackupCodeTokenClass(TokenClass):
+    """
+    The BUC token allows printing out valid OTP values.
+    This sheet of paper can be used to authenticate and strike out the used
+    OTP values. It works akin to TAN token (OTPs can be used in random order).
+    The difference is that the number and length of the generated OTPs can be
+    configured as well as the character pool for code generation. The default
+    settings: 10 codes, 16 characters long, generated from alphanumeric
+    charset. (a-zA-Z0-9)
+    """
+
+    @log_with(log)
+    def __init__(self, db_token):
+        """
+        This creates a new Backup Code Token object from a DB token object.
+
+        :param db_token: instance of the orm db object
+        :type db_token:  orm object
+        """
+        TokenClass.__init__(self, db_token)
+        self.set_type(u"backupcode")
+        self.hKeyRequired = False
+
+    @staticmethod
+    def get_class_type():
+        """
+        Return the token type shortname.
+
+        :return: 'backupcode'
+        :rtype: string
+        """
+        return "backupcode"
+
+    @staticmethod
+    def get_class_prefix():
+        """
+        Return the prefix, that is used as a prefix for the serial.
+        numbers
+        :return: BUC
+        """
+        return "BUC"
+
+    @staticmethod
+    @log_with(log)
+    def get_class_info(key=None, ret='all'):
+        """
+        Returns a subtree of the token definition
+
+        :param key: subsection identifier
+        :type key: string
+        :param ret: default return value, if nothing is found
+        :type ret: user defined
+        :return: subsection if key exists or user defined
+        :rtype: dict or scalar
+        """
+        res = {'type': 'backupcode',
+               'title': 'Backup Code Token',
+               'description': 'BUC: Backup codes generated by user.',
+               'init': {},
+               'config': {},
+               'user': ['enroll'],
+               'ui_enroll': ["admin", "user"],
+               'policy': {
+                   SCOPE.ENROLL: {
+                       BACKUPCODEACTION.BACKUPCODETOKEN_COUNT: {
+                           "type": "int",
+                           "desc": _("The number of OTP values, which are "
+                                     "generated by user.")
+                       },
+                       BACKUPCODEACTION.BACKUPCODETOKEN_LENGTH: {
+                           "type": "int",
+                           "desc": _("Length of each generated token.")
+                       },
+                       ACTION.MAXTOKENUSER: {
+                           'type': 'int',
+                           'desc': _(
+                               "The user may only have this maximum number "
+                               "of backup code tokens assigned."),
+                           'group': GROUP.TOKEN
+                       },
+                       ACTION.MAXACTIVETOKENUSER: {
+                           'type': 'int',
+                           'desc': _(
+                               "The user may only have this maximum number "
+                               "of active backup code tokens assigned."),
+                           'group': GROUP.TOKEN
+                       }
+
+                   }
+               }
+               }
+
+        if key:
+            ret = res.get(key, {})
+        else:
+            if ret == 'all':
+                ret = res
+        return ret
+
+    def generate_codes(self, length, count):
+        """
+        Generates a list with <count> number of alphanumeric codes with
+        length of <length> characters picked from CHARACTER_POOL.
+
+        :param length: length of each generated token in characters
+        :type length: int
+        :param count: total number of tokens to generate
+        :type count: int
+        :return: list of tokens generated with preconfigured length, number and
+                 character set
+        :rtype: list
+        """
+        return ["".join(SECRET_GENERATOR.choices(CHARACTER_POOL, k=length)) for
+                _ in range(count)]
+
+    def check_otp(self, otpval, counter=None, window=None, options=None):
+        """
+        Check if the given OTP value is valid for this token.
+
+        :param anOtpVal: the to be verified otpvalue
+        :type anOtpVal: string
+        :param counter: the counter state, that should be verified
+        :type counter: int
+        :param window: the counter +window, which should be checked
+        :type window: int
+        :param options: the dict, which could contain token specific
+        info
+        :type options: dict
+        :return: the counter state or -1
+        :rtype: int
+        """
+        token_info = self.get_tokeninfo()
+        all_user_codes = ast.literal_eval(token_info.get("otps", []))
+        used_codes = ast.literal_eval(token_info.get("used_otps", []))
+
+        if otpval in used_codes:
+            return -1
+
+        if otpval in all_user_codes:
+            used_codes.append(otpval)
+            token_info["used_otps"] = used_codes
+            self.set_tokeninfo(token_info)
+            return 1
+
+        return -1
+
+    @check_token_locked
+    def authenticate(self, passw, user=None, options=None):
+        """
+        High level interface which covers the check_pin and check_otp
+        This is the method that verifies single shot authentication
+        like they are done with push button tokens.
+
+        It is automatically called for OTPs longer than 6 characters.
+        Passw can be either a plain OTP token or a pin concatenated with an
+        OTP.
+        Pin can be either prepended in front of the OTP (default) - like
+        pinOTP or
+        it can be appended after the OTP - like OTPpin. Appending or prepending
+        a pin can be configured in the TokenClass config.
+
+        If the authentication succeeds an OTP counter needs to be
+        increased,
+        i.e. the OTP value that was used for this authentication is
+        invalidated!
+
+        :param passw: the password which could be pin+otp value
+        :type passw: string
+        :param user: The authenticating user
+        :type user: User object
+        :param options: dictionary of additional request parameters
+        :type options: dict
+
+        :return: returns tuple of
+
+            1. true or false for the pin match,
+            2. the otpcounter (int) and the
+            3. reply (dict) that will be added as additional
+            information in
+                the JSON response of ``/validate/check``.
+
+        :rtype: tuple(bool, int, dict)
+        """
+        is_pin_correct = False
+        otp_counter = -1
+        reply = None
+
+        (is_split_successfully, pin, otpval) = self.split_pin_pass(passw,
+                                                                   user=user,
+                                                                   options=options)
+        if is_split_successfully:
+            is_pin_correct = self.check_pin(pin, user=user, options=options)
+            if is_pin_correct:
+                otp_counter = self.check_otp(otpval, options=options)
+
+        return is_pin_correct, otp_counter, reply
+
+    def update(self, param, reset_failcount=True):
+        """
+        Update the token object
+
+        :param param: a dictionary with different params
+        like keysize,description, genkey, otpkey, pin
+        :type: param: dict
+        """
+        token_count = int(param.get("backupcodetoken_count", DEFAULT_COUNT))
+        token_length = int(param.get("backupcodetoken_length", DEFAULT_LENGTH))
+        param["otplen"] = token_length
+        TokenClass.update(self, param, reset_failcount=reset_failcount)
+        backup_codes = self.generate_codes(length=token_length,
+                                           count=token_count)
+        self.add_init_details("otps", backup_codes)
+        self.add_tokeninfo("otps", backup_codes)
+        self.add_tokeninfo("used_otps", [])
diff --git a/privacyidea/static/components/token/views/token.enroll.backupcode.html b/privacyidea/static/components/token/views/token.enroll.backupcode.html
new file mode 100644
index 000000000..0fcc96e50
--- /dev/null
+++ b/privacyidea/static/components/token/views/token.enroll.backupcode.html
@@ -0,0 +1,5 @@
+<p class="help-block" translate>
+    The TAN token will let you print a list of OTP values.
+    These OTP values can be used to authenticate. The values can be used in an
+    arbitrary order.
+</p>
\ No newline at end of file
diff --git a/privacyidea/static/components/token/views/token.enrolled.backupcode.html b/privacyidea/static/components/token/views/token.enrolled.backupcode.html
new file mode 100644
index 000000000..7c1889a48
--- /dev/null
+++ b/privacyidea/static/components/token/views/token.enrolled.backupcode.html
@@ -0,0 +1,58 @@
+<div class="row">
+    <div class="col-sm-12">
+        <uib-accordion close-others="oneAtATime">
+            <div uib-accordion-group
+                 class="panel-default"
+                 heading="{{ 'The OTP values'|translate }}">
+                <div id="paperOtpTable">
+                    <div ng-include="instanceUrl+'/'+piCustomization+
+                    '/views/includes/token.enrolled.tan.top.html'"></div>
+                    <div class="table-responsive">
+                        <table class="table table-bordered table-striped
+                                  tantoken">
+                            <thead>
+                            <tr>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <tr ng-repeat="key in otp_rows">
+                                <td>{{ $index }}</td>
+                                <td>{{ enrolledToken.otps[$index] }}</td>
+                                <td>{{ $index+(1*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (1*otp_row_count)] }}</td>
+                                <td>{{ $index+(2*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (2*otp_row_count)] }}</td>
+                                <td>{{ $index+(3*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (3*otp_row_count)] }}</td>
+                                <td>{{ $index+(4*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (4*otp_row_count)] }}</td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                    <div ng-include="instanceUrl+'/'+piCustomization+
+                    '/views/includes/token.enrolled.tan.bottom.html'"></div>
+                </div>
+            </div>
+        </uib-accordion>
+    </div>
+</div>
+<button class="btn-default btn"
+    ng-click="printOtp()">
+    <span class="glyphicon glyphicon-print"></span>
+    <span translate>Print the OTP list</span>
+</button>
\ No newline at end of file
diff --git a/tests/test_lib_tokens_backupcode.py b/tests/test_lib_tokens_backupcode.py
new file mode 100644
index 000000000..2e3103bef
--- /dev/null
+++ b/tests/test_lib_tokens_backupcode.py
@@ -0,0 +1,150 @@
+"""
+This test file tests the lib.tokens.backupcodetoken
+This depends on lib.tokenclass
+"""
+import ast
+
+from privacyidea.lib.token import init_token
+from privacyidea.lib.tokens.backupcodetoken import BackupCodeTokenClass
+from privacyidea.lib.tokens.backupcodetoken import DEFAULT_COUNT
+from privacyidea.lib.tokens.backupcodetoken import DEFAULT_LENGTH
+from privacyidea.models import Token
+from .base import MyTestCase
+
+"""
+ Tokens need to be of configured length in order for pin-parsing to work.
+ Pin and otp value are passed as a single string which is then split based on
+ the length of the OTP.
+"""
+VALID_OTP_1 = "a" * DEFAULT_LENGTH
+VALID_OTP_2 = "b" * DEFAULT_LENGTH
+INVALID_OTP = "n" * DEFAULT_LENGTH
+
+VALID_PIN = "valid_pin"
+INVALID_PIN = "invalid_pin"
+
+SUCCESS = 1
+FAILURE = -1
+
+
+class BackupCodeTokenTestCase(MyTestCase):
+    serial1 = "ser1"
+
+    def get_fresh_token(self, set_pin=False):
+        token = init_token({"type": "backupcode"})
+        token.set_tokeninfo(
+            {"otps": f"['{VALID_OTP_1}', '{VALID_OTP_2}']", "used_otps": "[]"})
+
+        if set_pin:
+            token.set_pin(VALID_PIN)
+
+        return token
+
+    def test_01_create_token(self):
+        db_token = Token(self.serial1, tokentype="backupcode")
+        db_token.save()
+        token = BackupCodeTokenClass(db_token)
+        token.update({})
+
+        self.assertEqual(token.token.serial, self.serial1)
+        self.assertEqual(token.token.tokentype, "backupcode")
+        self.assertEqual(token.type, "backupcode")
+
+        class_prefix = token.get_class_prefix()
+
+        self.assertEqual(class_prefix, "BUC")
+        self.assertEqual(token.get_class_type(), "backupcode")
+
+    def test_02_class_methods(self):
+        db_token = Token.query.filter(Token.serial == self.serial1).first()
+        token = BackupCodeTokenClass(db_token)
+
+        info = token.get_class_info()
+        self.assertEqual(info.get("title"), "Backup Code Token")
+
+        info = token.get_class_info("title")
+        self.assertEqual(info, "Backup Code Token")
+
+    def test_03_get_init_details(self):
+        db_token = Token.query.filter(Token.serial == self.serial1).first()
+        token = BackupCodeTokenClass(db_token)
+        token.update({})
+
+        # make sure OTPs were created
+        init_detail = token.get_init_detail()
+        self.assertTrue("otps" in init_detail)
+
+        # make sure there's correct (configured) number of generated OTPs
+        frontend_otps = init_detail.get("otps")
+        backend_otps = ast.literal_eval(token.get_tokeninfo("otps"))
+        self.assertEqual(frontend_otps, backend_otps)
+        self.assertTrue(len(frontend_otps) == DEFAULT_COUNT)
+
+        used_otps = ast.literal_eval(token.get_tokeninfo("used_otps"))
+        self.assertEqual(used_otps, [])
+
+        # make sure the generated OTPs are of correct (configured) length
+        for otp in frontend_otps:
+            self.assertTrue(len(otp) == DEFAULT_LENGTH)
+
+    def test_04_check_unused_existent_otp(self):
+        token = self.get_fresh_token()
+        valid_otp_check_result = token.check_otp(VALID_OTP_1)
+        used_otps = ast.literal_eval(token.get_tokeninfo("used_otps"))
+
+        self.assertEquals(valid_otp_check_result, SUCCESS)
+        self.assertEquals(used_otps, [VALID_OTP_1])
+
+    def test_05_check_used_existent_otp(self):
+        token = self.get_fresh_token()
+
+        self.assertEquals(token.check_otp(VALID_OTP_1), SUCCESS)
+        self.assertEquals(token.check_otp(VALID_OTP_1), FAILURE)
+
+    def test_06_check_non_existent_otp(self):
+        token = self.get_fresh_token()
+        all_otps = ast.literal_eval(token.get_tokeninfo("otps"))
+
+        self.assertTrue(INVALID_OTP not in all_otps)
+        self.assertEquals(token.check_otp(INVALID_OTP), FAILURE)
+
+    def test_07_authenticate_with_correct_pin_and_correct_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = VALID_PIN + VALID_OTP_1
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, SUCCESS)
+
+    def test_08_authenticate_with_incorrect_pin_and_correct_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = INVALID_PIN + VALID_OTP_1
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertFalse(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
+
+    def test_08_authenticate_with_correct_pin_and_incorrect_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = VALID_PIN + INVALID_OTP
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
+
+    def test_08_authenticate_without_pin_correct_otp(self):
+        token = self.get_fresh_token()
+
+        is_pin_correct, otp_auth_result, _ = token.authenticate(VALID_OTP_1)
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, SUCCESS)
+
+    def test_08_authenticate_without_pin_incorrect_otp(self):
+        token = self.get_fresh_token()
+
+        is_pin_correct, otp_auth_result, _ = token.authenticate(INVALID_OTP)
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
-- 
GitLab


From 8e29bfce2650b2a71f9c4fb8d24057b833e5d1a1 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 10 Oct 2022 15:57:19 +0200
Subject: [PATCH 03/26] feat:
 replaced_token_registration_with_py_webauthn_method

---
 .../tokens/{webauthn.py => webauthn_new.py}   | 57 ++++++++++++++++-
 privacyidea/lib/tokens/webauthntoken.py       | 61 +++++++++++++++----
 2 files changed, 104 insertions(+), 14 deletions(-)
 rename privacyidea/lib/tokens/{webauthn.py => webauthn_new.py} (96%)

diff --git a/privacyidea/lib/tokens/webauthn.py b/privacyidea/lib/tokens/webauthn_new.py
similarity index 96%
rename from privacyidea/lib/tokens/webauthn.py
rename to privacyidea/lib/tokens/webauthn_new.py
index 07586874a..36b4455f4 100644
--- a/privacyidea/lib/tokens/webauthn.py
+++ b/privacyidea/lib/tokens/webauthn_new.py
@@ -71,7 +71,12 @@ from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, MGF
 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
 from cryptography.hazmat.primitives.hashes import SHA256
 from cryptography.x509 import load_der_x509_certificate
+from webauthn import verify_registration_response, base64url_to_bytes
+from webauthn.helpers import bytes_to_base64url
+from webauthn.helpers.exceptions import InvalidRegistrationResponse
+from webauthn.helpers.structs import RegistrationCredential
 
+from privacyidea.lib import challenge
 from privacyidea.lib.tokens.u2f import url_encode, url_decode
 from privacyidea.lib.utils import to_bytes, to_unicode
 
@@ -116,6 +121,7 @@ class ATTESTATION_TYPE(object):
     NONE = 'None'
     ATTESTATION_CA = 'AttCA'
     SELF_ATTESTATION = 'Self'
+    ANONYMIZATION_CA = 'AnonCA'
 
 
 # Only supporting 'None', 'Basic', and 'Self Attestation' attestation types for now.
@@ -1188,6 +1194,17 @@ class WebAuthnRegistrationResponse(object):
                 aaguid
             )
 
+    def extract_credential_id(self):
+        # decoded_response = dict(
+        #     [(k, webauthn_b64_decode(v)) for k, v in self.registration_response.items()])
+        attestation_object = self.registration_response["attObj"]
+        decoded_attestation_object = cbor2.loads(webauthn_b64_decode(attestation_object))
+        auth_data = decoded_attestation_object["authData"]
+        attestation_data = auth_data[37:]
+        credential_id_len = struct.unpack('!H', attestation_data[16:18])[0]
+        credential_id = attestation_data[18:18 + credential_id_len]
+        return credential_id
+
     def verify(self, existing_credential_ids=None):
         """
         Verify the WebAuthnRegistrationResponse.
@@ -1199,9 +1216,47 @@ class WebAuthnRegistrationResponse(object):
         :param existing_credential_ids: A list of existing credential ids to check for duplicates.
         :type existing_credential_ids: basestring[]
         :return: The WebAuthnCredential produced by the registration ceremony.
-        :rtype: WebAuthnCredential
+        :rtype: VerifiedRegistration
         """
 
+        try:
+            credential_raw_id = self.extract_credential_id()
+            credential_id = bytes_to_base64url(credential_raw_id)
+            response = {
+                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+            }
+
+            raw_registration_response = {
+                # need to make id == base64url(rawID)
+                "rawId": credential_raw_id,
+                "id": credential_id,
+                "response": response
+            }
+
+            verified_registration = verify_registration_response(
+                credential=RegistrationCredential.parse_obj(raw_registration_response),
+                expected_challenge=base64url_to_bytes(self.challenge),
+                expected_rp_id=self.rp_id,
+                expected_origin=self.origin,
+                require_user_verification=self.uv_required
+            )
+            return verified_registration
+            credential = WebAuthnCredential(
+                rp_id=self.rp_id,
+                origin=self.origin,
+                aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
+                credential_id=verified_registration.credential_id,
+                public_key=verified_registration.credential_public_key,
+                sign_count=verified_registration.sign_count,
+                # extract from fmt or attestation_object?
+                attestation_level="none",
+                attestation_cert=None
+            )
+            return credential
+        except InvalidRegistrationResponse as ex:
+            raise RegistrationRejectedException from ex
+
         try:
             # Step 1.
             #
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 85eb0a511..f43589ef6 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -22,29 +22,37 @@
 #
 
 import binascii
+import logging
 
 from OpenSSL import crypto
 from cryptography import x509
+from webauthn.helpers import bytes_to_base64url
 
 from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
+from privacyidea.lib import _
 from privacyidea.lib.challenge import get_challenges
 from privacyidea.lib.config import get_from_config
 from privacyidea.lib.crypto import geturandom
 from privacyidea.lib.decorators import check_token_locked
-from privacyidea.lib.error import ParameterError, RegistrationError, PolicyError
+from privacyidea.lib.error import ParameterError, RegistrationError, \
+    PolicyError
+from privacyidea.lib.log import log_with
+from privacyidea.lib.policy import SCOPE, GROUP, ACTION
 from privacyidea.lib.token import get_tokens
 from privacyidea.lib.tokenclass import TokenClass, CLIENTMODE, ROLLOUTSTATE
 from privacyidea.lib.tokens.u2f import x509name_to_string
-from privacyidea.lib.tokens.webauthn import (COSE_ALGORITHM, webauthn_b64_encode, WebAuthnRegistrationResponse,
-                                             ATTESTATION_REQUIREMENT_LEVEL, webauthn_b64_decode,
-                                             WebAuthnMakeCredentialOptions, WebAuthnAssertionOptions, WebAuthnUser,
-                                             WebAuthnAssertionResponse, AuthenticationRejectedException,
-                                             USER_VERIFICATION_LEVEL)
 from privacyidea.lib.tokens.u2ftoken import IMAGES
-from privacyidea.lib.log import log_with
-import logging
-from privacyidea.lib import _
-from privacyidea.lib.policy import SCOPE, GROUP, ACTION
+from privacyidea.lib.tokens.webauthn_new import (COSE_ALGORITHM,
+                                                 webauthn_b64_encode,
+                                                 WebAuthnRegistrationResponse,
+                                                 ATTESTATION_REQUIREMENT_LEVEL,
+                                                 webauthn_b64_decode,
+                                                 WebAuthnMakeCredentialOptions,
+                                                 WebAuthnAssertionOptions,
+                                                 WebAuthnUser,
+                                                 WebAuthnAssertionResponse,
+                                                 AuthenticationRejectedException,
+                                                 USER_VERIFICATION_LEVEL)
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import hexlify_and_unicode, is_true
 
@@ -495,6 +503,12 @@ class WEBAUTHNINFO(object):
     ATTESTATION_SUBJECT = "attestation_subject"
     RELYING_PARTY_ID = "relying_party_id"
     RELYING_PARTY_NAME = "relying_party_name"
+    FORMAT = "format"
+    CREDENTIAL_TYPE = "credential_type"
+    USER_VERIFIED = "user_verified"
+    ATTESTATION_OBJECT = "attestation_object"
+    CREDENTIAL_DEVICE_TYPE = "credential_device_type"
+    CREDENTIAL_BACKED_UP = "credential_backed_up"
 
 
 class WEBAUTHNGROUP(object):
@@ -866,10 +880,31 @@ class WebAuthnTokenClass(TokenClass):
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
 
-            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(web_authn_credential.credential_id)))
+            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id))))
             self.set_otp_count(web_authn_credential.sign_count)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(web_authn_credential.public_key)))
+                               hexlify_and_unicode(webauthn_b64_decode(
+                                   bytes_to_base64url(
+                                       web_authn_credential.credential_public_key))))
+            self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
+                               hexlify_and_unicode(
+                                   web_authn_credential.aaguid))
+            self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
+                               web_authn_credential.fmt)
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
+                               web_authn_credential.credential_type)
+            self.add_tokeninfo(WEBAUTHNINFO.USER_VERIFIED,
+                               web_authn_credential.user_verified)
+            self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_OBJECT,
+                               bytes_to_base64url(web_authn_credential.attestation_object))
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_DEVICE_TYPE,
+                               web_authn_credential.credential_device_type)
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
+                               web_authn_credential.credential_device_type)
+
+            '''
+            self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
+                               hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.public_key))))
             self.add_tokeninfo(WEBAUTHNINFO.ORIGIN,
                                web_authn_credential.origin)
             self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_LEVEL,
@@ -898,7 +933,7 @@ class WebAuthnTokenClass(TokenClass):
 
                 cn = web_authn_credential.attestation_cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
                 automatic_description = cn[0].value if len(cn) else None
-
+            '''
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
             if not self.token.description:
-- 
GitLab


From 71eb550a8696ed3383878109dae5bce6b7f97cc2 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 10 Oct 2022 17:05:04 +0200
Subject: [PATCH 04/26] feat: removed_hexlification_of_keys

---
 privacyidea/lib/tokens/webauthntoken.py | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index f43589ef6..d8b8eec18 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -785,7 +785,7 @@ class WebAuthnTokenClass(TokenClass):
             user_display_name=str(user),
             icon_url=IMAGES.get(self.token.description.lower().split()[0], "") if self.token.description else "",
             credential_id=self.decrypt_otpkey(),
-            public_key=webauthn_b64_encode(binascii.unhexlify(self.get_tokeninfo(WEBAUTHNINFO.PUB_KEY))),
+            public_key=self.get_tokeninfo(WEBAUTHNINFO.PUB_KEY),
             sign_count=self.get_otp_count(),
             rp_id=self.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID)
         )
@@ -804,7 +804,7 @@ class WebAuthnTokenClass(TokenClass):
         :rtype: basestring
         """
 
-        return webauthn_b64_encode(binascii.unhexlify(self.token.get_otpkey().getKey()))
+        return webauthn_b64_encode(self.token.get_otpkey().getKey())
 
     def update(self, param, reset_failcount=True):
         """
@@ -880,12 +880,11 @@ class WebAuthnTokenClass(TokenClass):
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
 
-            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id))))
+            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id)))
             self.set_otp_count(web_authn_credential.sign_count)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(
                                    bytes_to_base64url(
-                                       web_authn_credential.credential_public_key))))
+                                       web_authn_credential.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
                                hexlify_and_unicode(
                                    web_authn_credential.aaguid))
-- 
GitLab


From 80d0570fd3ce63107c9ebc7804ba78439ca4cd4b Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 09:41:23 +0200
Subject: [PATCH 05/26] feat:
 replaced_user_authentication_with_py_webauthn_method

---
 privacyidea/lib/tokens/webauthntoken.py | 39 +++++++++++++++++++++++--
 1 file changed, 37 insertions(+), 2 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index d8b8eec18..995ae2ef8 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -26,7 +26,10 @@ import logging
 
 from OpenSSL import crypto
 from cryptography import x509
+from webauthn import verify_authentication_response, base64url_to_bytes
 from webauthn.helpers import bytes_to_base64url
+from webauthn.helpers.exceptions import InvalidAuthenticationResponse
+from webauthn.helpers.structs import AuthenticationCredential
 
 from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
 from privacyidea.lib import _
@@ -1207,7 +1210,6 @@ class WebAuthnTokenClass(TokenClass):
         :return: A numerical value where values larger than zero indicate success.
         :rtype: int
         """
-
         if is_webauthn_assertion_response(options) and getParam(options, "challenge", optional):
             credential_id = getParam(options, "credentialid", required)
             authenticator_data = getParam(options, "authenticatordata", required)
@@ -1215,12 +1217,45 @@ class WebAuthnTokenClass(TokenClass):
             signature_data = getParam(options, "signaturedata", required)
             user_handle = getParam(options, "userhandle", optional)
             assertion_client_extensions = getParam(options, "assertionclientextensions", optional)
-
             try:
                 user = self._get_webauthn_user(getParam(options, "user", required))
             except ParameterError:
                 raise ValueError("When performing WebAuthn authorization, options must contain user")
 
+            try:
+                auth_id = credential_id
+                auth_raw_id = base64url_to_bytes(auth_id)
+
+                response = {
+                    "authenticatorData": webauthn_b64_decode(authenticator_data),
+                    "clientDataJSON": webauthn_b64_decode(client_data),
+                    "signature": webauthn_b64_decode(signature_data),
+                    "userHandle": user_handle
+                }
+                raw_auth_response = {
+                    "rawId": auth_raw_id,
+                    "id": auth_id,
+                    "response": response
+                }
+
+                verified_authentication = verify_authentication_response(
+                    credential=AuthenticationCredential.parse_obj(
+                        raw_auth_response),
+                    expected_challenge=binascii.unhexlify(options["challenge"]),
+                    expected_rp_id=user.rp_id,
+                    expected_origin=options["HTTP_ORIGIN"],
+                    credential_public_key=user.public_key,
+                    credential_current_sign_count=user.sign_count,
+                    require_user_verification=options.get(
+                        WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, False)
+                )
+                sign_count = verified_authentication.new_sign_count
+                self.set_otp_count(sign_count)
+                return sign_count
+
+            except InvalidAuthenticationResponse as ex:
+                raise AuthenticationRejectedException from ex
+
             uv_req = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
 
             challenge = binascii.unhexlify(getParam(options, "challenge", required))
-- 
GitLab


From d64c8bd7a3f288487769c240aaf59bf087b98eb6 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 11:07:51 +0200
Subject: [PATCH 06/26] refactor: removed_dead_code

---
 privacyidea/lib/tokens/webauthn_new.py  | 234 ------------------------
 privacyidea/lib/tokens/webauthntoken.py | 109 +----------
 2 files changed, 4 insertions(+), 339 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthn_new.py b/privacyidea/lib/tokens/webauthn_new.py
index 36b4455f4..41b50bdcc 100644
--- a/privacyidea/lib/tokens/webauthn_new.py
+++ b/privacyidea/lib/tokens/webauthn_new.py
@@ -1257,240 +1257,6 @@ class WebAuthnRegistrationResponse(object):
         except InvalidRegistrationResponse as ex:
             raise RegistrationRejectedException from ex
 
-        try:
-            # Step 1.
-            #
-            # Let JSONtext be the result of running UTF-8 decode on the value of
-            # response.clientDataJSON
-            json_text = self.registration_response.get('clientData', '')
-
-            # Step 2.
-            #
-            # Let C, the client data claimed as collected during the credential
-            # creation, be the result of running an implementation-specific JSON
-            # parser on JSONtext.
-            decoded_cd = webauthn_b64_decode(json_text)
-            c = json.loads(to_unicode(decoded_cd))
-
-            # Step 3.
-            #
-            # Verify that the value of C.type is webauthn.create.
-            if not _verify_type(c.get('type'), CLIENT_DATA_TYPE.CREATE):
-                raise RegistrationRejectedException('Invalid type.')
-
-            # Step 4.
-            #
-            # Verify that the value of C.challenge matches the challenge that was sent
-            # to the authenticator in the create() call.
-            if not _verify_challenge(c.get('challenge'), self.challenge):
-                raise RegistrationRejectedException('Unable to verify challenge.')
-
-            # Step 5.
-            #
-            # Verify that the value of C.origin matches the Relying Party's origin.
-            if not _verify_origin(c, self.origin):
-                raise RegistrationRejectedException('Unable to verify origin.')
-
-            # Step 6.
-            #
-            # Verify that the value of C.tokenBinding.status matches the state of
-            # Token Binding for the TLS connection over which the assertion was
-            # obtained. If Token Binding was used on that TLS connection, also verify
-            # that C.tokenBinding.id matches the base64url encoding of the Token
-            # Binding ID for the connection.
-
-            # Chrome does not currently supply token binding in the clientDataJSON
-            # if not _verify_token_binding_id(c):
-            #     raise RegistrationRejectedException('Unable to verify token binding ID.')
-
-            # Step 7.
-            #
-            # Compute the hash of response.clientDataJSON using SHA-256.
-            client_data_hash = _get_client_data_hash(decoded_cd)
-
-            # Step 8.
-            #
-            # Perform CBOR decoding on the attestationObject field of
-            # the AuthenticatorAttestationResponse structure to obtain
-            # the attestation statement format fmt, the authenticator
-            # data authData, and the attestation statement attStmt.
-            att_obj = self.parse_attestation_object(self.registration_response.get('attObj'))
-            att_stmt = att_obj.get('attStmt')
-            auth_data = att_obj.get('authData')
-            fmt = att_obj.get('fmt')
-            if not auth_data or len(auth_data) < 37:
-                raise RegistrationRejectedException('Auth data must be at least 37 bytes.')
-
-            # Step 9.
-            #
-            # Verify that the RP ID hash in authData is indeed the
-            # SHA-256 hash of the RP ID expected by the RP.
-            if not _verify_rp_id_hash(_get_auth_data_rp_id_hash(auth_data), self.rp_id):
-                raise RegistrationRejectedException('Unable to verify RP ID hash.')
-
-            # Step 10.
-            #
-            # Verify that the User Present bit of the flags in authData
-            # is set.
-            if not AuthenticatorDataFlags(auth_data).user_present:
-                raise RegistrationRejectedException('Malformed request received.')
-
-            # Step 11.
-            #
-            # If user verification is required for this registration, verify
-            # that the User Verified bit of the flags in authData is set.
-            if self.uv_required and not AuthenticatorDataFlags(auth_data).user_verified:
-                raise RegistrationRejectedException('Malformed request received.')
-
-            # Step 12.
-            #
-            # Verify that the values of the client extension outputs in
-            # clientExtensionResults and the authenticator extension outputs
-            # in the extensions in authData are as expected, considering the
-            # client extension input values that were given as the extensions
-            # option in the create() call. In particular, any extension
-            # identifier values in the clientExtensionResults and the extensions
-            # in authData MUST be also be present as extension identifier values
-            # in the extensions member of options, i.e., no extensions are
-            # present that were not requested. In the general case, the meaning
-            # of "are as expected" is specific to the Relying Party and which
-            # extensions are in use.
-            if not _verify_authenticator_extensions(auth_data, self.expected_registration_authenticator_extensions):
-                raise RegistrationRejectedException('Unable to verify authenticator extensions.')
-            if not _verify_client_extensions(
-                self.registration_response.get('registrationClientExtensions'),
-                self.expected_registration_client_extensions
-            ):
-                raise RegistrationRejectedException('Unable to verify client extensions.')
-
-            # Step 13.
-            #
-            # Determine the attestation statement format by performing
-            # a USASCII case-sensitive match on fmt against the set of
-            # supported WebAuthn Attestation Statement Format Identifier
-            # values. The up-to-date list of registered WebAuthn
-            # Attestation Statement Format Identifier values is maintained
-            # in the IANA registry of the same name.
-            if not _verify_attestation_statement_format(fmt):
-                raise RegistrationRejectedException('Unable to verify attestation statement format.')
-
-            # Step 14.
-            #
-            # Verify that attStmt is a correct attestation statement, conveying
-            # a valid attestation signature, by using the attestation statement
-            # format fmt's verification procedure given attStmt, authData and
-            # the hash of the serialized client data computed in step 7.
-            (
-                attestation_type,
-                trust_path,
-                credential_public_key,
-                cred_id,
-                aaguid
-            ) = self.verify_attestation_statement(
-                fmt,
-                att_stmt,
-                auth_data,
-                client_data_hash,
-                self.none_attestation_permitted
-            )
-            b64_cred_id = webauthn_b64_encode(cred_id)
-
-            # Step 15.
-            #
-            # If validation is successful, obtain a list of acceptable trust
-            # anchors (attestation root certificates or ECDAA-Issuer public
-            # keys) for that attestation type and attestation statement format
-            # fmt, from a trusted source or from policy. For example, the FIDO
-            # Metadata Service [FIDOMetadataService] provides one way to obtain
-            # such information, using the aaguid in the attestedCredentialData
-            # in authData.
-            trust_anchors = _get_trust_anchors(attestation_type, fmt, self.trust_anchor_dir) \
-                if self.trust_anchor_dir \
-                else None
-            if not trust_anchors and self.trusted_attestation_cert_required:
-                raise RegistrationRejectedException('No trust anchors available to verify attestation certificate.')
-
-            # Step 16.
-            #
-            # Assess the attestation trustworthiness using the outputs of the
-            # verification procedure in step 14, as follows:
-            #
-            #     * If self attestation was used, check if self attestation is
-            #       acceptable under Relying Party policy.
-            #     * If ECDAA was used, verify that the identifier of the
-            #       ECDAA-Issuer public key used is included in the set of
-            #       acceptable trust anchors obtained in step 15.
-            #     * Otherwise, use the X.509 certificates returned by the
-            #       verification procedure to verify that the attestation
-            #       public key correctly chains up to an acceptable root
-            #       certificate.
-            if attestation_type == ATTESTATION_TYPE.SELF_ATTESTATION and not self.self_attestation_permitted:
-                raise RegistrationRejectedException('Self attestation is not permitted.')
-            is_trusted_attestation_cert = ((attestation_type == ATTESTATION_TYPE.BASIC
-                                            and _is_trusted_x509_attestation_cert(trust_path, trust_anchors))
-                                           or (attestation_type == ATTESTATION_TYPE.ECDAA
-                                               and _is_trusted_ecdaa_attestation_certificate(None, trust_anchors)))
-            is_signed_attestation_cert = attestation_type in SUPPORTED_ATTESTATION_TYPES
-
-            if is_trusted_attestation_cert:
-                attestation_level = ATTESTATION_LEVEL.TRUSTED
-            elif is_signed_attestation_cert:
-                attestation_level = ATTESTATION_LEVEL.UNTRUSTED
-            else:
-                attestation_level = ATTESTATION_LEVEL.NONE
-
-            # Step 17.
-            #
-            # Check that the credentialId is not yet registered to any other user.
-            # If registration is requested for a credential that is already registered
-            # to a different user, the Relying Party SHOULD fail this registration
-            # ceremony, or it MAY decide to accept the registration, e.g. while deleting
-            # the older registration.
-            if existing_credential_ids and b64_cred_id in existing_credential_ids:
-                raise RegistrationRejectedException('Credential already exists.')
-
-            # Step 18.
-            #
-            # If the attestation statement attStmt verified successfully and is
-            # found to be trustworthy, then register the new credential with the
-            # account that was denoted in the options.user passed to create(),
-            # by associating it with the credentialId and credentialPublicKey in
-            # the attestedCredentialData in authData, as appropriate for the
-            # Relying Party's system.
-            credential = WebAuthnCredential(rp_id=self.rp_id,
-                                            origin=self.origin,
-                                            aaguid=aaguid,
-                                            credential_id=b64_cred_id,
-                                            public_key=webauthn_b64_encode(credential_public_key),
-                                            sign_count=struct.unpack('!I', auth_data[33:37])[0],
-                                            attestation_level=attestation_level,
-                                            attestation_cert=trust_path[0] if trust_path else None)
-            if is_trusted_attestation_cert:
-                return credential
-
-            # Step 19.
-            #
-            # If the attestation statement attStmt successfully verified but is
-            # not trustworthy per step 16 above, the Relying Party SHOULD fail
-            # the registration ceremony.
-            #
-            #     NOTE: However, if permitted by policy, the Relying Party MAY
-            #           register the credential ID and credential public key but
-            #           treat the credential as one with self attestation (see
-            #           6.3.3 Attestation Types). If doing so, the Relying Party
-            #           is asserting there is no cryptographic proof that the
-            #           public key credential has been generated by a particular
-            #           authenticator model. See [FIDOSecRef] and [UAFProtocol]
-            #           for a more detailed discussion.
-            if self.trusted_attestation_cert_required:
-                raise RegistrationRejectedException('Untrusted attestation certificate.')
-            if not is_signed_attestation_cert and not self.none_attestation_permitted:
-                raise RegistrationRejectedException('No (or unsupported) attestation certificate.')
-            return credential
-
-        except Exception as e:
-            raise RegistrationRejectedException('Registration rejected. Error: {}'.format(e))
-
 
 class WebAuthnAssertionResponse(object):
     """
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 995ae2ef8..df099ce4f 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -882,7 +882,6 @@ class WebAuthnTokenClass(TokenClass):
             ).verify([
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
-
             self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id)))
             self.set_otp_count(web_authn_credential.sign_count)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
@@ -904,38 +903,6 @@ class WebAuthnTokenClass(TokenClass):
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
                                web_authn_credential.credential_device_type)
 
-            '''
-            self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.public_key))))
-            self.add_tokeninfo(WEBAUTHNINFO.ORIGIN,
-                               web_authn_credential.origin)
-            self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_LEVEL,
-                               web_authn_credential.attestation_level)
-
-            self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
-                               hexlify_and_unicode(web_authn_credential.aaguid))
-
-            # Add attestation info.
-            if web_authn_credential.attestation_cert:
-                # attestation_cert is of type X509. If you get warnings from your IDE
-                # here, it is because your IDE mistakenly assumes it to be of type PKey,
-                # due to a bug in pyOpenSSL 18.0.0. This bug is – however – purely
-                # cosmetic (a wrongly hinted return type in X509.from_cryptography()),
-                # and can be safely ignored.
-                #
-                # See also:
-                # https://github.com/pyca/pyopenssl/commit/4121e2555d07bbba501ac237408a0eea1b41f467
-                attestation_cert = crypto.X509.from_cryptography(web_authn_credential.attestation_cert)
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER,
-                                   x509name_to_string(attestation_cert.get_issuer()))
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT,
-                                   x509name_to_string(attestation_cert.get_subject()))
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL,
-                                   attestation_cert.get_serial_number())
-
-                cn = web_authn_credential.attestation_cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
-                automatic_description = cn[0].value if len(cn) else None
-            '''
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
             if not self.token.description:
@@ -1216,7 +1183,8 @@ class WebAuthnTokenClass(TokenClass):
             client_data = getParam(options, "clientdata", required)
             signature_data = getParam(options, "signaturedata", required)
             user_handle = getParam(options, "userhandle", optional)
-            assertion_client_extensions = getParam(options, "assertionclientextensions", optional)
+            uv_required = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
+
             try:
                 user = self._get_webauthn_user(getParam(options, "user", required))
             except ParameterError:
@@ -1246,85 +1214,16 @@ class WebAuthnTokenClass(TokenClass):
                     expected_origin=options["HTTP_ORIGIN"],
                     credential_public_key=user.public_key,
                     credential_current_sign_count=user.sign_count,
-                    require_user_verification=options.get(
-                        WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, False)
+                    require_user_verification=uv_required
                 )
                 sign_count = verified_authentication.new_sign_count
                 self.set_otp_count(sign_count)
+
                 return sign_count
 
             except InvalidAuthenticationResponse as ex:
                 raise AuthenticationRejectedException from ex
 
-            uv_req = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
-
-            challenge = binascii.unhexlify(getParam(options, "challenge", required))
-
-            try:
-                try:
-                    http_origin = getParam(options, "HTTP_ORIGIN", required, allow_empty=False)
-                except ParameterError:
-                    raise AuthenticationRejectedException('HTTP Origin header missing.')
-
-                # This does the heavy lifting.
-                #
-                # All data is parsed and verified. If any errors occur, an exception
-                # will be raised.
-                self.set_otp_count(WebAuthnAssertionResponse(
-                                       webauthn_user=user,
-                                       assertion_response={
-                                           'id': credential_id,
-                                           'userHandle': user_handle,
-                                           'clientData': client_data,
-                                           'authData': authenticator_data,
-                                           'signature': signature_data,
-                                           'assertionClientExtensions':
-                                               webauthn_b64_decode(assertion_client_extensions)
-                                                   if assertion_client_extensions
-                                                   else None
-                                       },
-                                       challenge=webauthn_b64_encode(challenge),
-                                       origin=http_origin,
-                                       allow_credentials=[user.credential_id],
-                                       uv_required=uv_req
-                                   ).verify())
-            except AuthenticationRejectedException as e:
-                # The authentication ceremony failed.
-                log.warning("Checking response for token {0!s} failed. {1!s}".format(self.token.serial, e))
-                return -1
-
-            # At this point we can check, if the attestation certificate is
-            # authorized. If not, we can raise a policy exception.
-            if not attestation_certificate_allowed(
-                    {
-                        "attestation_issuer": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER),
-                        "attestation_serial": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL),
-                        "attestation_subject": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT)
-                    },
-                    getParam(options, WEBAUTHNACTION.REQ, optional)
-            ):
-                log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy restriction {1!s}"
-                        .format(self.token.serial, WEBAUTHNACTION.REQ))
-                raise PolicyError("The WebAuthn token is not allowed to authenticate due to a policy restriction.")
-
-            # Now we need to check, if a whitelist for AAGUIDs exists, and if
-            # so, if this device is whitelisted. If not, we again raise a
-            # policy exception.
-            allowed_aaguids = getParam(options, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST, optional)
-            if allowed_aaguids and self.get_tokeninfo(WEBAUTHNINFO.AAGUID) not in allowed_aaguids:
-                log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy restriction {1!s}"
-                        .format(self.token.serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-                raise PolicyError("The WebAuthn token is not allowed to authenticate due to a policy restriction.")
-
-            # All clear? Nice!
-            return self.get_otp_count()
-
-        else:
-            # Not all necessary data provided.
-            return -1
-
 
 def is_webauthn_assertion_response(request_data):
     """
-- 
GitLab


From 52f762dbedb48f4974414a331dfeb90a8fd1241f Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 11:41:27 +0200
Subject: [PATCH 07/26] fix: added_repeated_registration_check_on_credential

---
 privacyidea/lib/tokens/webauthn_new.py | 100 +++++++++++++------------
 1 file changed, 52 insertions(+), 48 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthn_new.py b/privacyidea/lib/tokens/webauthn_new.py
index 41b50bdcc..7c5e802dc 100644
--- a/privacyidea/lib/tokens/webauthn_new.py
+++ b/privacyidea/lib/tokens/webauthn_new.py
@@ -1195,67 +1195,71 @@ class WebAuthnRegistrationResponse(object):
             )
 
     def extract_credential_id(self):
-        # decoded_response = dict(
-        #     [(k, webauthn_b64_decode(v)) for k, v in self.registration_response.items()])
         attestation_object = self.registration_response["attObj"]
         decoded_attestation_object = cbor2.loads(webauthn_b64_decode(attestation_object))
         auth_data = decoded_attestation_object["authData"]
         attestation_data = auth_data[37:]
         credential_id_len = struct.unpack('!H', attestation_data[16:18])[0]
         credential_id = attestation_data[18:18 + credential_id_len]
+
         return credential_id
 
-    def verify(self, existing_credential_ids=None):
-        """
-        Verify the WebAuthnRegistrationResponse.
 
-        This will perform the registration ceremony for the
-        WebAuthnRegistrationResponse. It will only return on successful
-        verification. In any other case, an appropriate error will be raised.
+def verify(self, existing_credential_ids=None):
+    """
+    Verify the WebAuthnRegistrationResponse.
 
-        :param existing_credential_ids: A list of existing credential ids to check for duplicates.
-        :type existing_credential_ids: basestring[]
-        :return: The WebAuthnCredential produced by the registration ceremony.
-        :rtype: VerifiedRegistration
-        """
+    This will perform the registration ceremony for the
+    WebAuthnRegistrationResponse. It will only return on successful
+    verification. In any other case, an appropriate error will be raised.
 
-        try:
-            credential_raw_id = self.extract_credential_id()
-            credential_id = bytes_to_base64url(credential_raw_id)
-            response = {
-                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
-                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
-            }
+    :param existing_credential_ids: A list of existing credential ids to check for duplicates.
+    :type existing_credential_ids: basestring[]
+    :return: The WebAuthnCredential produced by the registration ceremony.
+    :rtype: VerifiedRegistration
+    """
 
-            raw_registration_response = {
-                # need to make id == base64url(rawID)
-                "rawId": credential_raw_id,
-                "id": credential_id,
-                "response": response
-            }
+    try:
+        credential_raw_id = self.extract_credential_id()
+        credential_id = bytes_to_base64url(credential_raw_id)
 
-            verified_registration = verify_registration_response(
-                credential=RegistrationCredential.parse_obj(raw_registration_response),
-                expected_challenge=base64url_to_bytes(self.challenge),
-                expected_rp_id=self.rp_id,
-                expected_origin=self.origin,
-                require_user_verification=self.uv_required
-            )
-            return verified_registration
-            credential = WebAuthnCredential(
-                rp_id=self.rp_id,
-                origin=self.origin,
-                aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
-                credential_id=verified_registration.credential_id,
-                public_key=verified_registration.credential_public_key,
-                sign_count=verified_registration.sign_count,
-                # extract from fmt or attestation_object?
-                attestation_level="none",
-                attestation_cert=None
-            )
-            return credential
-        except InvalidRegistrationResponse as ex:
-            raise RegistrationRejectedException from ex
+        if existing_credential_ids and credential_id in existing_credential_ids:
+            raise RegistrationRejectedException('Credential already exists.')
+
+        response = {
+            "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+            "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+        }
+
+        raw_registration_response = {
+            # need to make id == base64url(rawID)
+            "rawId": credential_raw_id,
+            "id": credential_id,
+            "response": response
+        }
+
+        verified_registration = verify_registration_response(
+            credential=RegistrationCredential.parse_obj(raw_registration_response),
+            expected_challenge=base64url_to_bytes(self.challenge),
+            expected_rp_id=self.rp_id,
+            expected_origin=self.origin,
+            require_user_verification=self.uv_required
+        )
+        return verified_registration
+        credential = WebAuthnCredential(
+            rp_id=self.rp_id,
+            origin=self.origin,
+            aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
+            credential_id=verified_registration.credential_id,
+            public_key=verified_registration.credential_public_key,
+            sign_count=verified_registration.sign_count,
+            # extract from fmt or attestation_object?
+            attestation_level="none",
+            attestation_cert=None
+        )
+        return credential
+    except InvalidRegistrationResponse as ex:
+        raise RegistrationRejectedException from ex
 
 
 class WebAuthnAssertionResponse(object):
-- 
GitLab


From 6aec3ce10811c97579f0377c23efd31e86a456e5 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 13:42:18 +0200
Subject: [PATCH 08/26] fix: integrated_new_return_type_of_verify_method

---
 .../tokens/{webauthn_new.py => webauthn.py}   | 99 +++++++++----------
 privacyidea/lib/tokens/webauthntoken.py       | 59 ++++++-----
 tests/test_lib_tokens_webauthn.py             | 18 ++--
 3 files changed, 85 insertions(+), 91 deletions(-)
 rename privacyidea/lib/tokens/{webauthn_new.py => webauthn.py} (96%)

diff --git a/privacyidea/lib/tokens/webauthn_new.py b/privacyidea/lib/tokens/webauthn.py
similarity index 96%
rename from privacyidea/lib/tokens/webauthn_new.py
rename to privacyidea/lib/tokens/webauthn.py
index 7c5e802dc..0f79c87df 100644
--- a/privacyidea/lib/tokens/webauthn_new.py
+++ b/privacyidea/lib/tokens/webauthn.py
@@ -66,8 +66,10 @@ from cryptography import x509
 from cryptography.exceptions import InvalidSignature
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import constant_time
-from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1, ECDSA
-from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, MGF1
+from cryptography.hazmat.primitives.asymmetric.ec import \
+    EllipticCurvePublicNumbers, SECP256R1, ECDSA
+from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, \
+    MGF1
 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
 from cryptography.hazmat.primitives.hashes import SHA256
 from cryptography.x509 import load_der_x509_certificate
@@ -75,8 +77,9 @@ from webauthn import verify_registration_response, base64url_to_bytes
 from webauthn.helpers import bytes_to_base64url
 from webauthn.helpers.exceptions import InvalidRegistrationResponse
 from webauthn.helpers.structs import RegistrationCredential
+from webauthn.registration.verify_registration_response import \
+    VerifiedRegistration
 
-from privacyidea.lib import challenge
 from privacyidea.lib.tokens.u2f import url_encode, url_decode
 from privacyidea.lib.utils import to_bytes, to_unicode
 
@@ -1205,61 +1208,53 @@ class WebAuthnRegistrationResponse(object):
         return credential_id
 
 
-def verify(self, existing_credential_ids=None):
-    """
-    Verify the WebAuthnRegistrationResponse.
-
-    This will perform the registration ceremony for the
-    WebAuthnRegistrationResponse. It will only return on successful
-    verification. In any other case, an appropriate error will be raised.
+    def verify(self, existing_credential_ids=None):
+        """
+        Verify the WebAuthnRegistrationResponse.
+
+        This will perform the registration ceremony for the
+        WebAuthnRegistrationResponse. It will only return on successful
+        verification. In any other case, an appropriate error will
+        be raised.
+
+        :param existing_credential_ids: A list of existing
+        credential ids to check for duplicates.
+        :type existing_credential_ids: basestring[]
+        :return: The VerifiedRegistration produced by the registration
+        ceremony.
+        :rtype: VerifiedRegistration
+        """
 
-    :param existing_credential_ids: A list of existing credential ids to check for duplicates.
-    :type existing_credential_ids: basestring[]
-    :return: The WebAuthnCredential produced by the registration ceremony.
-    :rtype: VerifiedRegistration
-    """
+        try:
+            credential_raw_id = self.extract_credential_id()
+            credential_id = bytes_to_base64url(credential_raw_id)
 
-    try:
-        credential_raw_id = self.extract_credential_id()
-        credential_id = bytes_to_base64url(credential_raw_id)
+            if existing_credential_ids and credential_id in existing_credential_ids:
+                raise RegistrationRejectedException('Credential already exists.')
 
-        if existing_credential_ids and credential_id in existing_credential_ids:
-            raise RegistrationRejectedException('Credential already exists.')
+            response = {
+                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+            }
 
-        response = {
-            "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
-            "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
-        }
+            raw_registration_response = {
+                # need to make id == base64url(rawID)
+                "rawId": credential_raw_id,
+                "id": credential_id,
+                "response": response
+            }
 
-        raw_registration_response = {
-            # need to make id == base64url(rawID)
-            "rawId": credential_raw_id,
-            "id": credential_id,
-            "response": response
-        }
+            verified_registration = verify_registration_response(
+                credential=RegistrationCredential.parse_obj(raw_registration_response),
+                expected_challenge=base64url_to_bytes(self.challenge),
+                expected_rp_id=self.rp_id,
+                expected_origin=self.origin,
+                require_user_verification=self.uv_required
+            )
+            return verified_registration
 
-        verified_registration = verify_registration_response(
-            credential=RegistrationCredential.parse_obj(raw_registration_response),
-            expected_challenge=base64url_to_bytes(self.challenge),
-            expected_rp_id=self.rp_id,
-            expected_origin=self.origin,
-            require_user_verification=self.uv_required
-        )
-        return verified_registration
-        credential = WebAuthnCredential(
-            rp_id=self.rp_id,
-            origin=self.origin,
-            aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
-            credential_id=verified_registration.credential_id,
-            public_key=verified_registration.credential_public_key,
-            sign_count=verified_registration.sign_count,
-            # extract from fmt or attestation_object?
-            attestation_level="none",
-            attestation_cert=None
-        )
-        return credential
-    except InvalidRegistrationResponse as ex:
-        raise RegistrationRejectedException from ex
+        except InvalidRegistrationResponse as ex:
+            raise RegistrationRejectedException from ex
 
 
 class WebAuthnAssertionResponse(object):
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index df099ce4f..16451e83d 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -24,38 +24,33 @@
 import binascii
 import logging
 
-from OpenSSL import crypto
-from cryptography import x509
 from webauthn import verify_authentication_response, base64url_to_bytes
 from webauthn.helpers import bytes_to_base64url
 from webauthn.helpers.exceptions import InvalidAuthenticationResponse
 from webauthn.helpers.structs import AuthenticationCredential
 
-from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
+from privacyidea.api.lib.utils import getParam
 from privacyidea.lib import _
 from privacyidea.lib.challenge import get_challenges
 from privacyidea.lib.config import get_from_config
 from privacyidea.lib.crypto import geturandom
 from privacyidea.lib.decorators import check_token_locked
-from privacyidea.lib.error import ParameterError, RegistrationError, \
-    PolicyError
+from privacyidea.lib.error import ParameterError, RegistrationError
 from privacyidea.lib.log import log_with
 from privacyidea.lib.policy import SCOPE, GROUP, ACTION
 from privacyidea.lib.token import get_tokens
 from privacyidea.lib.tokenclass import TokenClass, CLIENTMODE, ROLLOUTSTATE
-from privacyidea.lib.tokens.u2f import x509name_to_string
 from privacyidea.lib.tokens.u2ftoken import IMAGES
-from privacyidea.lib.tokens.webauthn_new import (COSE_ALGORITHM,
-                                                 webauthn_b64_encode,
-                                                 WebAuthnRegistrationResponse,
-                                                 ATTESTATION_REQUIREMENT_LEVEL,
-                                                 webauthn_b64_decode,
-                                                 WebAuthnMakeCredentialOptions,
-                                                 WebAuthnAssertionOptions,
-                                                 WebAuthnUser,
-                                                 WebAuthnAssertionResponse,
-                                                 AuthenticationRejectedException,
-                                                 USER_VERIFICATION_LEVEL)
+from privacyidea.lib.tokens.webauthn import (COSE_ALGORITHM,
+                                             webauthn_b64_encode,
+                                             WebAuthnRegistrationResponse,
+                                             ATTESTATION_REQUIREMENT_LEVEL,
+                                             webauthn_b64_decode,
+                                             WebAuthnMakeCredentialOptions,
+                                             WebAuthnAssertionOptions,
+                                             WebAuthnUser,
+                                             AuthenticationRejectedException,
+                                             USER_VERIFICATION_LEVEL)
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import hexlify_and_unicode, is_true
 
@@ -864,7 +859,7 @@ class WebAuthnTokenClass(TokenClass):
             #
             # All data is parsed and verified. If any errors occur an exception
             # will be raised.
-            web_authn_credential = WebAuthnRegistrationResponse(
+            verified_registration = WebAuthnRegistrationResponse(
                 rp_id=rp_id,
                 origin=http_origin,
                 registration_response={
@@ -882,26 +877,29 @@ class WebAuthnTokenClass(TokenClass):
             ).verify([
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
-            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id)))
-            self.set_otp_count(web_authn_credential.sign_count)
+
+            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(verified_registration.credential_id)))
+            self.set_otp_count(verified_registration.sign_count)
+            self.add_tokeninfo(WEBAUTHNINFO.ORIGIN, http_origin)
+            self.add_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID, rp_id)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
                                    bytes_to_base64url(
-                                       web_authn_credential.credential_public_key))
+                                       verified_registration.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
                                hexlify_and_unicode(
-                                   web_authn_credential.aaguid))
+                                   verified_registration.aaguid))
             self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
-                               web_authn_credential.fmt)
+                               verified_registration.fmt)
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
-                               web_authn_credential.credential_type)
+                               verified_registration.credential_type)
             self.add_tokeninfo(WEBAUTHNINFO.USER_VERIFIED,
-                               web_authn_credential.user_verified)
+                               verified_registration.user_verified)
             self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_OBJECT,
-                               bytes_to_base64url(web_authn_credential.attestation_object))
+                               bytes_to_base64url(verified_registration.attestation_object))
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_DEVICE_TYPE,
-                               web_authn_credential.credential_device_type)
+                               verified_registration.credential_device_type)
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
-                               web_authn_credential.credential_device_type)
+                               verified_registration.credential_device_type)
 
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
@@ -1212,7 +1210,7 @@ class WebAuthnTokenClass(TokenClass):
                     expected_challenge=binascii.unhexlify(options["challenge"]),
                     expected_rp_id=user.rp_id,
                     expected_origin=options["HTTP_ORIGIN"],
-                    credential_public_key=user.public_key,
+                    credential_public_key=base64url_to_bytes(user.public_key),
                     credential_current_sign_count=user.sign_count,
                     require_user_verification=uv_required
                 )
@@ -1222,7 +1220,8 @@ class WebAuthnTokenClass(TokenClass):
                 return sign_count
 
             except InvalidAuthenticationResponse as ex:
-                raise AuthenticationRejectedException from ex
+                log.warning(ex)
+                return -1
 
 
 def is_webauthn_assertion_response(request_data):
diff --git a/tests/test_lib_tokens_webauthn.py b/tests/test_lib_tokens_webauthn.py
index 6321d59e8..29b40ff69 100644
--- a/tests/test_lib_tokens_webauthn.py
+++ b/tests/test_lib_tokens_webauthn.py
@@ -379,7 +379,7 @@ class WebAuthnTokenTestCase(MyTestCase):
 
 class WebAuthnTestCase(unittest.TestCase):
     @staticmethod
-    def getWebAuthnCredential():
+    def getVerifiedRegistration():
         return WebAuthnRegistrationResponse(
             rp_id=RP_ID,
             origin=ORIGIN,
@@ -393,16 +393,16 @@ class WebAuthnTestCase(unittest.TestCase):
 
     @staticmethod
     def getAssertionResponse():
-        credential = WebAuthnTestCase.getWebAuthnCredential()
+        verified_registration = WebAuthnTestCase.getVerifiedRegistration()
         webauthn_user = WebAuthnUser(
             user_id=USER_ID,
             user_name=USER_NAME,
             user_display_name=USER_DISPLAY_NAME,
             icon_url=ICON_URL,
-            credential_id=credential.credential_id.decode(),
-            public_key=credential.public_key,
-            sign_count=credential.sign_count,
-            rp_id=credential.rp_id
+            credential_id=verified_registration.credential_id.decode(),
+            public_key=verified_registration.credential_public_key,
+            sign_count=verified_registration.sign_count,
+            rp_id=RP_ID
         )
 
         webauthn_assertion_response = WebAuthnAssertionResponse(
@@ -438,9 +438,9 @@ class WebAuthnTestCase(unittest.TestCase):
         self.assertTrue(CRED_KEY in registration_dict['pubKeyCredParams'])
 
     def test_01_validate_registration(self):
-        web_authn_credential = self.getWebAuthnCredential()
-        self.assertEqual(RP_ID, web_authn_credential.rp_id)
-        self.assertEqual(ORIGIN, web_authn_credential.origin)
+        verified_registration = self.getVerifiedRegistration()
+        self.assertEqual(CRED_ID, verified_registration.credential_id)
+        self.assertEqual(CRED_KEY, verified_registration.credential_public_key)
 
     def test_02_registration_invalid_user_verification(self):
         registration_response = WebAuthnRegistrationResponse(
-- 
GitLab


From 62ae0d21914954077a8ea8a4bdf984ef2dd30d32 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 14:03:06 +0200
Subject: [PATCH 09/26] fix: allowed_all_webauthntokens_in_api_prepolicy

---
 privacyidea/api/lib/prepolicy.py | 61 --------------------------------
 1 file changed, 61 deletions(-)

diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
index 5c2fff3a9..2fdd087df 100644
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -2022,67 +2022,6 @@ def webauthntoken_allowed(request, action):
     :return:
     :rtype:
     """
-
-    ttype = request.all_data.get("type")
-
-    # Get the registration data of the 2nd step of enrolling a WebAuthn token
-    reg_data = request.all_data.get("regdata")
-
-    # If a WebAuthn token is being enrolled.
-    if ttype and ttype.lower() == WebAuthnTokenClass.get_class_type() and reg_data:
-        serial = request.all_data.get("serial")
-        att_obj = WebAuthnRegistrationResponse.parse_attestation_object(reg_data)
-        (
-            attestation_type,
-            trust_path,
-            credential_pub_key,
-            cred_id,
-            aaguid
-        ) = WebAuthnRegistrationResponse.verify_attestation_statement(fmt=att_obj.get('fmt'),
-                                                                      att_stmt=att_obj.get('attStmt'),
-                                                                      auth_data=att_obj.get('authData'))
-
-        attestation_cert = crypto.X509.from_cryptography(trust_path[0]) if trust_path else None
-        allowed_certs_pols = Match\
-            .user(g,
-                  scope=SCOPE.ENROLL,
-                  action=WEBAUTHNACTION.REQ,
-                  user_object=request.User if hasattr(request, 'User') else None) \
-            .action_values(unique=False)
-
-        allowed_aaguids_pols = Match \
-            .user(g,
-                  scope=SCOPE.ENROLL,
-                  action=WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST,
-                  user_object=request.User if hasattr(request, 'User') else None) \
-            .action_values(unique=False,
-                           allow_white_space_in_action=True)
-        allowed_aaguids = set(
-            aaguid
-            for allowed_aaguid_pol in allowed_aaguids_pols
-            for aaguid in allowed_aaguid_pol.split()
-        )
-
-        # attestation_cert is of type X509. If you get a warning from your IDE
-        # here, it is because your IDE mistakenly assumes it to be of type PKey,
-        # due to a bug in pyOpenSSL 18.0.0. This bug is – however – purely
-        # cosmetic (a wrongly hinted return type in X509.from_cryptography()),
-        # and can be safely ignored.
-        #
-        # See also:
-        # https://github.com/pyca/pyopenssl/commit/4121e2555d07bbba501ac237408a0eea1b41f467
-        if allowed_certs_pols and not _attestation_certificate_allowed(attestation_cert, allowed_certs_pols):
-            log.warning(
-                "The WebAuthn token {0!s} is not allowed to be registered due to policy restriction {1!s}"
-                    .format(serial, WEBAUTHNACTION.REQ))
-            raise PolicyError("The WebAuthn token is not allowed to be registered due to a policy restriction.")
-
-        if allowed_aaguids and aaguid not in [allowed_aaguid.replace("-", "") for allowed_aaguid in allowed_aaguids]:
-            log.warning(
-                "The WebAuthn token {0!s} is not allowed to be registered due to policy restriction {1!s}"
-                    .format(serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-            raise PolicyError("The WebAuthn token is not allowed to be registered due to a policy restriction.")
-
     return True
 
 
-- 
GitLab


From 73672f0ede66c3213a29be7bb7e591e38d7fe2c3 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 19 Oct 2022 09:03:56 +0200
Subject: [PATCH 10/26] fix:
 removed_redundant_aaguid_modification_in_token_info

---
 privacyidea/lib/tokens/webauthntoken.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 16451e83d..765a6e7f9 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -886,8 +886,7 @@ class WebAuthnTokenClass(TokenClass):
                                    bytes_to_base64url(
                                        verified_registration.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
-                               hexlify_and_unicode(
-                                   verified_registration.aaguid))
+                               verified_registration.aaguid)
             self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
                                verified_registration.fmt)
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
-- 
GitLab


From 10b0a279770fcb570664c0dbfdbd481b674fc82b Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 28 Sep 2022 15:16:20 +0200
Subject: [PATCH 11/26] fix: make_tests_run_on_windows

---
 privacyidea/config.py | 31 +++++++++++++++++++++----------
 privacyidea/models.py |  4 ++--
 2 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/privacyidea/config.py b/privacyidea/config.py
index 15015c262..9e388b868 100644
--- a/privacyidea/config.py
+++ b/privacyidea/config.py
@@ -1,14 +1,15 @@
 # -*- coding: utf-8 -*-
 
-import os
 import logging
+import os
 import random
 import string
+from sys import platform
+
 log = logging.getLogger(__name__)
 basedir = os.path.abspath(os.path.dirname(__file__))
 basedir = "/".join(basedir.split("/")[:-1]) + "/"
 
-
 pubtest_key = b"""-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0stF+PVh/qR7I82ywgDW
 X2rPxKnlBugU33SAbxZjfNz+aKYYM+irKJ0IdTOLtQ85DzZaEhocqO7cg1Qw85Ac
@@ -34,7 +35,8 @@ WQIDAQAB
 def _random_password(size):
     log.info("SECRET_KEY not set in config. Generating a random key.")
     passwd = [random.choice(string.ascii_lowercase + \
-                            string.ascii_uppercase + string.digits) for _x in range(size)]
+                            string.ascii_uppercase + string.digits) for _x in
+              range(size)]
     # return shuffled password
     random.shuffle(passwd)
     return "".join(passwd)
@@ -44,7 +46,8 @@ class Config(object):
     SECRET_KEY = os.environ.get('SECRET_KEY')
     # SQL_ALCHEMY_DATABASE_URI = "mysql://privacyidea:XmbSrlqy5d4IS08zjz"
     # "GG5HTt40Cpf5@localhost/privacyidea"
-    PI_ENCFILE = os.path.join(basedir, "tests/testdata/enckey")
+    # PI_ENCFILE = os.path.join(basedir, "tests/testdata/enckey")
+    PI_ENCFILE = os.path.join("tests/testdata/enckey")
     PI_HSM = "default"
     PI_AUDIT_MODULE = "privacyidea.lib.auditmodules.sqlaudit"
     PI_AUDIT_KEY_PRIVATE = os.path.join(basedir, "tests/testdata/private.pem")
@@ -65,7 +68,8 @@ class DevelopmentConfig(Config):
     DEBUG = True
     SECRET_KEY = os.environ.get('SECRET_KEY') or 't0p s3cr3t'
     SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
+                              'sqlite:///' + os.path.join(basedir,
+                                                          'data-dev.sqlite')
     PI_LOGLEVEL = logging.DEBUG
     PI_TRANSLATION_WARNING = "[Missing]"
 
@@ -75,8 +79,14 @@ class TestingConfig(Config):
     # This is used to encrypt the auth token
     SUPERUSER_REALM = ['adminrealm']
     SECRET_KEY = 'secret'
-    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
+    SQLITE_DB_PATH = 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
+    if platform == 'win32':
+        SQLITE_DB_PATH = 'sqlite:///' + os.path.join(basedir, 'Users',
+                                                     os.getlogin(),
+                                                     'Documents',
+                                                     'data-test.sqlite')
+    SQLALCHEMY_DATABASE_URI = os.environ.get(
+        'TEST_DATABASE_URL') or SQLITE_DB_PATH
     # This is used to encrypt the admin passwords
     PI_PEPPER = ""
     # This is only for testing encrypted files
@@ -124,8 +134,9 @@ class AltUIConfig(TestingConfig):
 
 class ProductionConfig(Config):
     SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data.sqlite')
-    #SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
+                              'sqlite:///' + os.path.join(basedir,
+                                                          'data.sqlite')
+    # SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
     # This is used to encrypt the auth_token
     SECRET_KEY = os.environ.get('SECRET_KEY') or _random_password(24)
     # This is used to encrypt the admin passwords
@@ -144,7 +155,7 @@ class HerokuConfig(Config):
                               "wqy_btZE3CPPNWsmkfdmeorxy6@" \
                               "ec2-54-83-0-61.compute-1." \
                               "amazonaws.com:5432/d6fjidokoeilp6"
-    #SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
+    # SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
     # This is used to encrypt the auth_token
     SECRET_KEY = os.environ.get('SECRET_KEY') or 't0p s3cr3t'
     # This is used to encrypt the admin passwords
diff --git a/privacyidea/models.py b/privacyidea/models.py
index 35dda42b3..3e78a7945 100644
--- a/privacyidea/models.py
+++ b/privacyidea/models.py
@@ -107,10 +107,10 @@ def save_config_timestamp(invalidate_config=True):
     """
     c1 = Config.query.filter_by(Key=PRIVACYIDEA_TIMESTAMP).first()
     if c1:
-        c1.Value = datetime.now().strftime("%s")
+        c1.Value = datetime.now().strftime("%S")
     else:
         new_timestamp = Config(PRIVACYIDEA_TIMESTAMP,
-                               datetime.now().strftime("%s"),
+                               datetime.now().strftime("%S"),
                                Description="config timestamp. last changed.")
         db.session.add(new_timestamp)
     if invalidate_config:
-- 
GitLab


From 91f86320ad2c596d108432644b9407caf5df9e90 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 28 Sep 2022 15:16:36 +0200
Subject: [PATCH 12/26] feat: backup_code_token

---
 privacyidea/lib/config.py                     |   1 +
 privacyidea/lib/tokens/backupcodetoken.py     | 265 ++++++++++++++++++
 .../token/views/token.enroll.backupcode.html  |   5 +
 .../views/token.enrolled.backupcode.html      |  58 ++++
 tests/test_lib_tokens_backupcode.py           | 150 ++++++++++
 5 files changed, 479 insertions(+)
 create mode 100644 privacyidea/lib/tokens/backupcodetoken.py
 create mode 100644 privacyidea/static/components/token/views/token.enroll.backupcode.html
 create mode 100644 privacyidea/static/components/token/views/token.enrolled.backupcode.html
 create mode 100644 tests/test_lib_tokens_backupcode.py

diff --git a/privacyidea/lib/config.py b/privacyidea/lib/config.py
index 819100544..10a948078 100644
--- a/privacyidea/lib/config.py
+++ b/privacyidea/lib/config.py
@@ -699,6 +699,7 @@ def get_token_list():
     """
     module_list = set()
 
+    module_list.add("privacyidea.lib.tokens.backupcodetoken")
     module_list.add("privacyidea.lib.tokens.daplugtoken")
     module_list.add("privacyidea.lib.tokens.hotptoken")
     module_list.add("privacyidea.lib.tokens.motptoken")
diff --git a/privacyidea/lib/tokens/backupcodetoken.py b/privacyidea/lib/tokens/backupcodetoken.py
new file mode 100644
index 000000000..7d64dfbf7
--- /dev/null
+++ b/privacyidea/lib/tokens/backupcodetoken.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+#
+#  2022-09-22 Peter Bolha <485456@mail.muni.cz>
+#             BUC token with to be randomly used BUC values
+#
+#  (c) 2022 Peter Bolha - 485456@mail.muni.cz
+#
+# This code is free software; you can redistribute it and/or
+# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or any later version.
+#
+# This code is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+This file contains the definition of the backupcode token class
+It depends on the DB model, and the lib.tokenclass.
+"""
+
+import ast
+import logging
+import secrets
+import string
+
+from privacyidea.lib import _
+from privacyidea.lib.decorators import check_token_locked
+from privacyidea.lib.log import log_with
+from privacyidea.lib.policy import ACTION, GROUP, SCOPE
+from privacyidea.lib.tokenclass import TokenClass
+
+log = logging.getLogger(__name__)
+
+# number of generated tokens per method call
+DEFAULT_COUNT = 10
+# length of each token
+DEFAULT_LENGTH = 16
+CHARACTER_POOL = list(string.digits) + list(string.ascii_letters)
+SECRET_GENERATOR = secrets.SystemRandom()
+
+
+class BACKUPCODEACTION(object):
+    BACKUPCODETOKEN_COUNT = "backupcodetoken_count"
+    BACKUPCODETOKEN_LENGTH = "backupcodetoken_length"
+
+
+class BackupCodeTokenClass(TokenClass):
+    """
+    The BUC token allows printing out valid OTP values.
+    This sheet of paper can be used to authenticate and strike out the used
+    OTP values. It works akin to TAN token (OTPs can be used in random order).
+    The difference is that the number and length of the generated OTPs can be
+    configured as well as the character pool for code generation. The default
+    settings: 10 codes, 16 characters long, generated from alphanumeric
+    charset. (a-zA-Z0-9)
+    """
+
+    @log_with(log)
+    def __init__(self, db_token):
+        """
+        This creates a new Backup Code Token object from a DB token object.
+
+        :param db_token: instance of the orm db object
+        :type db_token:  orm object
+        """
+        TokenClass.__init__(self, db_token)
+        self.set_type(u"backupcode")
+        self.hKeyRequired = False
+
+    @staticmethod
+    def get_class_type():
+        """
+        Return the token type shortname.
+
+        :return: 'backupcode'
+        :rtype: string
+        """
+        return "backupcode"
+
+    @staticmethod
+    def get_class_prefix():
+        """
+        Return the prefix, that is used as a prefix for the serial.
+        numbers
+        :return: BUC
+        """
+        return "BUC"
+
+    @staticmethod
+    @log_with(log)
+    def get_class_info(key=None, ret='all'):
+        """
+        Returns a subtree of the token definition
+
+        :param key: subsection identifier
+        :type key: string
+        :param ret: default return value, if nothing is found
+        :type ret: user defined
+        :return: subsection if key exists or user defined
+        :rtype: dict or scalar
+        """
+        res = {'type': 'backupcode',
+               'title': 'Backup Code Token',
+               'description': 'BUC: Backup codes generated by user.',
+               'init': {},
+               'config': {},
+               'user': ['enroll'],
+               'ui_enroll': ["admin", "user"],
+               'policy': {
+                   SCOPE.ENROLL: {
+                       BACKUPCODEACTION.BACKUPCODETOKEN_COUNT: {
+                           "type": "int",
+                           "desc": _("The number of OTP values, which are "
+                                     "generated by user.")
+                       },
+                       BACKUPCODEACTION.BACKUPCODETOKEN_LENGTH: {
+                           "type": "int",
+                           "desc": _("Length of each generated token.")
+                       },
+                       ACTION.MAXTOKENUSER: {
+                           'type': 'int',
+                           'desc': _(
+                               "The user may only have this maximum number "
+                               "of backup code tokens assigned."),
+                           'group': GROUP.TOKEN
+                       },
+                       ACTION.MAXACTIVETOKENUSER: {
+                           'type': 'int',
+                           'desc': _(
+                               "The user may only have this maximum number "
+                               "of active backup code tokens assigned."),
+                           'group': GROUP.TOKEN
+                       }
+
+                   }
+               }
+               }
+
+        if key:
+            ret = res.get(key, {})
+        else:
+            if ret == 'all':
+                ret = res
+        return ret
+
+    def generate_codes(self, length, count):
+        """
+        Generates a list with <count> number of alphanumeric codes with
+        length of <length> characters picked from CHARACTER_POOL.
+
+        :param length: length of each generated token in characters
+        :type length: int
+        :param count: total number of tokens to generate
+        :type count: int
+        :return: list of tokens generated with preconfigured length, number and
+                 character set
+        :rtype: list
+        """
+        return ["".join(SECRET_GENERATOR.choices(CHARACTER_POOL, k=length)) for
+                _ in range(count)]
+
+    def check_otp(self, otpval, counter=None, window=None, options=None):
+        """
+        Check if the given OTP value is valid for this token.
+
+        :param anOtpVal: the to be verified otpvalue
+        :type anOtpVal: string
+        :param counter: the counter state, that should be verified
+        :type counter: int
+        :param window: the counter +window, which should be checked
+        :type window: int
+        :param options: the dict, which could contain token specific
+        info
+        :type options: dict
+        :return: the counter state or -1
+        :rtype: int
+        """
+        token_info = self.get_tokeninfo()
+        all_user_codes = ast.literal_eval(token_info.get("otps", []))
+        used_codes = ast.literal_eval(token_info.get("used_otps", []))
+
+        if otpval in used_codes:
+            return -1
+
+        if otpval in all_user_codes:
+            used_codes.append(otpval)
+            token_info["used_otps"] = used_codes
+            self.set_tokeninfo(token_info)
+            return 1
+
+        return -1
+
+    @check_token_locked
+    def authenticate(self, passw, user=None, options=None):
+        """
+        High level interface which covers the check_pin and check_otp
+        This is the method that verifies single shot authentication
+        like they are done with push button tokens.
+
+        It is automatically called for OTPs longer than 6 characters.
+        Passw can be either a plain OTP token or a pin concatenated with an
+        OTP.
+        Pin can be either prepended in front of the OTP (default) - like
+        pinOTP or
+        it can be appended after the OTP - like OTPpin. Appending or prepending
+        a pin can be configured in the TokenClass config.
+
+        If the authentication succeeds an OTP counter needs to be
+        increased,
+        i.e. the OTP value that was used for this authentication is
+        invalidated!
+
+        :param passw: the password which could be pin+otp value
+        :type passw: string
+        :param user: The authenticating user
+        :type user: User object
+        :param options: dictionary of additional request parameters
+        :type options: dict
+
+        :return: returns tuple of
+
+            1. true or false for the pin match,
+            2. the otpcounter (int) and the
+            3. reply (dict) that will be added as additional
+            information in
+                the JSON response of ``/validate/check``.
+
+        :rtype: tuple(bool, int, dict)
+        """
+        is_pin_correct = False
+        otp_counter = -1
+        reply = None
+
+        (is_split_successfully, pin, otpval) = self.split_pin_pass(passw,
+                                                                   user=user,
+                                                                   options=options)
+        if is_split_successfully:
+            is_pin_correct = self.check_pin(pin, user=user, options=options)
+            if is_pin_correct:
+                otp_counter = self.check_otp(otpval, options=options)
+
+        return is_pin_correct, otp_counter, reply
+
+    def update(self, param, reset_failcount=True):
+        """
+        Update the token object
+
+        :param param: a dictionary with different params
+        like keysize,description, genkey, otpkey, pin
+        :type: param: dict
+        """
+        token_count = int(param.get("backupcodetoken_count", DEFAULT_COUNT))
+        token_length = int(param.get("backupcodetoken_length", DEFAULT_LENGTH))
+        param["otplen"] = token_length
+        TokenClass.update(self, param, reset_failcount=reset_failcount)
+        backup_codes = self.generate_codes(length=token_length,
+                                           count=token_count)
+        self.add_init_details("otps", backup_codes)
+        self.add_tokeninfo("otps", backup_codes)
+        self.add_tokeninfo("used_otps", [])
diff --git a/privacyidea/static/components/token/views/token.enroll.backupcode.html b/privacyidea/static/components/token/views/token.enroll.backupcode.html
new file mode 100644
index 000000000..0fcc96e50
--- /dev/null
+++ b/privacyidea/static/components/token/views/token.enroll.backupcode.html
@@ -0,0 +1,5 @@
+<p class="help-block" translate>
+    The TAN token will let you print a list of OTP values.
+    These OTP values can be used to authenticate. The values can be used in an
+    arbitrary order.
+</p>
\ No newline at end of file
diff --git a/privacyidea/static/components/token/views/token.enrolled.backupcode.html b/privacyidea/static/components/token/views/token.enrolled.backupcode.html
new file mode 100644
index 000000000..7c1889a48
--- /dev/null
+++ b/privacyidea/static/components/token/views/token.enrolled.backupcode.html
@@ -0,0 +1,58 @@
+<div class="row">
+    <div class="col-sm-12">
+        <uib-accordion close-others="oneAtATime">
+            <div uib-accordion-group
+                 class="panel-default"
+                 heading="{{ 'The OTP values'|translate }}">
+                <div id="paperOtpTable">
+                    <div ng-include="instanceUrl+'/'+piCustomization+
+                    '/views/includes/token.enrolled.tan.top.html'"></div>
+                    <div class="table-responsive">
+                        <table class="table table-bordered table-striped
+                                  tantoken">
+                            <thead>
+                            <tr>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <tr ng-repeat="key in otp_rows">
+                                <td>{{ $index }}</td>
+                                <td>{{ enrolledToken.otps[$index] }}</td>
+                                <td>{{ $index+(1*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (1*otp_row_count)] }}</td>
+                                <td>{{ $index+(2*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (2*otp_row_count)] }}</td>
+                                <td>{{ $index+(3*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (3*otp_row_count)] }}</td>
+                                <td>{{ $index+(4*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (4*otp_row_count)] }}</td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                    <div ng-include="instanceUrl+'/'+piCustomization+
+                    '/views/includes/token.enrolled.tan.bottom.html'"></div>
+                </div>
+            </div>
+        </uib-accordion>
+    </div>
+</div>
+<button class="btn-default btn"
+    ng-click="printOtp()">
+    <span class="glyphicon glyphicon-print"></span>
+    <span translate>Print the OTP list</span>
+</button>
\ No newline at end of file
diff --git a/tests/test_lib_tokens_backupcode.py b/tests/test_lib_tokens_backupcode.py
new file mode 100644
index 000000000..2e3103bef
--- /dev/null
+++ b/tests/test_lib_tokens_backupcode.py
@@ -0,0 +1,150 @@
+"""
+This test file tests the lib.tokens.backupcodetoken
+This depends on lib.tokenclass
+"""
+import ast
+
+from privacyidea.lib.token import init_token
+from privacyidea.lib.tokens.backupcodetoken import BackupCodeTokenClass
+from privacyidea.lib.tokens.backupcodetoken import DEFAULT_COUNT
+from privacyidea.lib.tokens.backupcodetoken import DEFAULT_LENGTH
+from privacyidea.models import Token
+from .base import MyTestCase
+
+"""
+ Tokens need to be of configured length in order for pin-parsing to work.
+ Pin and otp value are passed as a single string which is then split based on
+ the length of the OTP.
+"""
+VALID_OTP_1 = "a" * DEFAULT_LENGTH
+VALID_OTP_2 = "b" * DEFAULT_LENGTH
+INVALID_OTP = "n" * DEFAULT_LENGTH
+
+VALID_PIN = "valid_pin"
+INVALID_PIN = "invalid_pin"
+
+SUCCESS = 1
+FAILURE = -1
+
+
+class BackupCodeTokenTestCase(MyTestCase):
+    serial1 = "ser1"
+
+    def get_fresh_token(self, set_pin=False):
+        token = init_token({"type": "backupcode"})
+        token.set_tokeninfo(
+            {"otps": f"['{VALID_OTP_1}', '{VALID_OTP_2}']", "used_otps": "[]"})
+
+        if set_pin:
+            token.set_pin(VALID_PIN)
+
+        return token
+
+    def test_01_create_token(self):
+        db_token = Token(self.serial1, tokentype="backupcode")
+        db_token.save()
+        token = BackupCodeTokenClass(db_token)
+        token.update({})
+
+        self.assertEqual(token.token.serial, self.serial1)
+        self.assertEqual(token.token.tokentype, "backupcode")
+        self.assertEqual(token.type, "backupcode")
+
+        class_prefix = token.get_class_prefix()
+
+        self.assertEqual(class_prefix, "BUC")
+        self.assertEqual(token.get_class_type(), "backupcode")
+
+    def test_02_class_methods(self):
+        db_token = Token.query.filter(Token.serial == self.serial1).first()
+        token = BackupCodeTokenClass(db_token)
+
+        info = token.get_class_info()
+        self.assertEqual(info.get("title"), "Backup Code Token")
+
+        info = token.get_class_info("title")
+        self.assertEqual(info, "Backup Code Token")
+
+    def test_03_get_init_details(self):
+        db_token = Token.query.filter(Token.serial == self.serial1).first()
+        token = BackupCodeTokenClass(db_token)
+        token.update({})
+
+        # make sure OTPs were created
+        init_detail = token.get_init_detail()
+        self.assertTrue("otps" in init_detail)
+
+        # make sure there's correct (configured) number of generated OTPs
+        frontend_otps = init_detail.get("otps")
+        backend_otps = ast.literal_eval(token.get_tokeninfo("otps"))
+        self.assertEqual(frontend_otps, backend_otps)
+        self.assertTrue(len(frontend_otps) == DEFAULT_COUNT)
+
+        used_otps = ast.literal_eval(token.get_tokeninfo("used_otps"))
+        self.assertEqual(used_otps, [])
+
+        # make sure the generated OTPs are of correct (configured) length
+        for otp in frontend_otps:
+            self.assertTrue(len(otp) == DEFAULT_LENGTH)
+
+    def test_04_check_unused_existent_otp(self):
+        token = self.get_fresh_token()
+        valid_otp_check_result = token.check_otp(VALID_OTP_1)
+        used_otps = ast.literal_eval(token.get_tokeninfo("used_otps"))
+
+        self.assertEquals(valid_otp_check_result, SUCCESS)
+        self.assertEquals(used_otps, [VALID_OTP_1])
+
+    def test_05_check_used_existent_otp(self):
+        token = self.get_fresh_token()
+
+        self.assertEquals(token.check_otp(VALID_OTP_1), SUCCESS)
+        self.assertEquals(token.check_otp(VALID_OTP_1), FAILURE)
+
+    def test_06_check_non_existent_otp(self):
+        token = self.get_fresh_token()
+        all_otps = ast.literal_eval(token.get_tokeninfo("otps"))
+
+        self.assertTrue(INVALID_OTP not in all_otps)
+        self.assertEquals(token.check_otp(INVALID_OTP), FAILURE)
+
+    def test_07_authenticate_with_correct_pin_and_correct_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = VALID_PIN + VALID_OTP_1
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, SUCCESS)
+
+    def test_08_authenticate_with_incorrect_pin_and_correct_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = INVALID_PIN + VALID_OTP_1
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertFalse(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
+
+    def test_08_authenticate_with_correct_pin_and_incorrect_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = VALID_PIN + INVALID_OTP
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
+
+    def test_08_authenticate_without_pin_correct_otp(self):
+        token = self.get_fresh_token()
+
+        is_pin_correct, otp_auth_result, _ = token.authenticate(VALID_OTP_1)
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, SUCCESS)
+
+    def test_08_authenticate_without_pin_incorrect_otp(self):
+        token = self.get_fresh_token()
+
+        is_pin_correct, otp_auth_result, _ = token.authenticate(INVALID_OTP)
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
-- 
GitLab


From ce80652ceeed3a97803432556ff1ef2b6e9b92fd Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 10 Oct 2022 15:57:19 +0200
Subject: [PATCH 13/26] feat:
 replaced_token_registration_with_py_webauthn_method

---
 .../tokens/{webauthn.py => webauthn_new.py}   | 129 +++---
 privacyidea/lib/tokens/webauthntoken.py       | 426 +++++++++---------
 2 files changed, 280 insertions(+), 275 deletions(-)
 rename privacyidea/lib/tokens/{webauthn.py => webauthn_new.py} (95%)

diff --git a/privacyidea/lib/tokens/webauthn.py b/privacyidea/lib/tokens/webauthn_new.py
similarity index 95%
rename from privacyidea/lib/tokens/webauthn.py
rename to privacyidea/lib/tokens/webauthn_new.py
index 72423b114..36b4455f4 100644
--- a/privacyidea/lib/tokens/webauthn.py
+++ b/privacyidea/lib/tokens/webauthn_new.py
@@ -69,9 +69,14 @@ from cryptography.hazmat.primitives import constant_time
 from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1, ECDSA
 from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, MGF1
 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
-from cryptography.hazmat.primitives.hashes import SHA256, SHA1
+from cryptography.hazmat.primitives.hashes import SHA256
 from cryptography.x509 import load_der_x509_certificate
+from webauthn import verify_registration_response, base64url_to_bytes
+from webauthn.helpers import bytes_to_base64url
+from webauthn.helpers.exceptions import InvalidRegistrationResponse
+from webauthn.helpers.structs import RegistrationCredential
 
+from privacyidea.lib import challenge
 from privacyidea.lib.tokens.u2f import url_encode, url_decode
 from privacyidea.lib.utils import to_bytes, to_unicode
 
@@ -116,6 +121,7 @@ class ATTESTATION_TYPE(object):
     NONE = 'None'
     ATTESTATION_CA = 'AttCA'
     SELF_ATTESTATION = 'Self'
+    ANONYMIZATION_CA = 'AnonCA'
 
 
 # Only supporting 'None', 'Basic', and 'Self Attestation' attestation types for now.
@@ -183,7 +189,6 @@ class COSE_ALGORITHM(object):
     ES256 = -7
     PS256 = -37
     RS256 = -257
-    RS1 = -65535  # for tests, otherwise unsupported
 
 
 SUPPORTED_COSE_ALGORITHMS = (
@@ -426,9 +431,9 @@ class WebAuthnMakeCredentialOptions(object):
         :type rp_id: basestring
         :param user_id: The ID for the user credential being generated. This is the privacyIDEA token serial.
         :type user_id: basestring
-        :param user_name: The username the user logs in with.
+        :param user_name: The user name the user logs in with.
         :type user_name: basestring
-        :param user_display_name: The human-readable name of the user.
+        :param user_display_name: The human readable name of the user.
         :type user_display_name: basestring
         :param icon_url: An optional icon url.
         :type icon_url: basestring
@@ -665,9 +670,9 @@ class WebAuthnUser(object):
 
         :param user_id: The ID for the user credential being stored. This is the privacyIDEA token serial.
         :type user_id: basestring
-        :param user_name: The username the user logs in with.
+        :param user_name: The user name the user logs in with.
         :type user_name: basestring
-        :param user_display_name: The human-readable name of the user.
+        :param user_display_name: The human readable name of the user.
         :type user_display_name: basestring
         :param icon_url: An optional icon url.
         :type icon_url: basestring
@@ -700,8 +705,7 @@ class WebAuthnUser(object):
         self.rp_id = rp_id
 
     def __str__(self):
-        return '{!r} ({}, {}, {})'.format(self.user_id, self.user_name,
-                                          self.user_display_name, self.sign_count)
+        return '{} ({}, {}, {})'.format(self.user_id, self.user_name, self.user_display_name, self.sign_count)
 
 
 class WebAuthnCredential(object):
@@ -772,9 +776,8 @@ class WebAuthnCredential(object):
 
         return not ATTESTATION_REQUIREMENT_LEVEL[self.attestation_level]['self_attestation_permitted']
 
-    def __str__(self):
-        return '{!r} ({}, {}, {})'.format(self.credential_id, self.rp_id,
-                                          self.origin, self.sign_count)
+    def __str_(self):
+        return '{} ({}, {}, {})'.format(self.credential_id, self.rp_id, self.origin, self.sign_count)
 
 
 class WebAuthnRegistrationResponse(object):
@@ -1168,56 +1171,13 @@ class WebAuthnRegistrationResponse(object):
                 #   * If successful, return attestation type ECDAA and
                 #     attestation trust path ecdaaKeyId.
                 raise RegistrationRejectedException('ECDAA attestation type is not currently supported.')
-            else:
-                # self-attestation
-                # Step 1:
-                # Validate that alg matches the algorithm of the credentialPublicKey
-                # in authenticatorData.
-                try:
-                    public_key_alg, credential_public_key = _load_cose_public_key(credential_pub_key)
-                except COSEKeyException as e:
-                    raise RegistrationRejectedException(str(e))
-                if alg != public_key_alg:
-                    raise RegistrationRejectedException('credentialPublicKey algorithm {0!s} does '
-                                                        'not match algorithm from attestation '
-                                                        'statement {1!s}'.format(public_key_alg, alg))
-                # Step 2:
-                # Verify that sig is a valid signature over the concatenation of authenticatorData
-                # and clientDataHash using the credential public key with alg.
-                try:
-                    _verify_signature(credential_public_key, alg, verification_data, signature)
-                except InvalidSignature:
-                    raise RegistrationRejectedException('Invalid signature received.')
-                except NotImplementedError:  # pragma: no cover
-                    log.warning('Unsupported algorithm ({0!s}) for signature '
-                                'verification'.format(alg))
-                    # We do not support this algorithm. Treat as none attestation, if acceptable.
-                    if none_attestation_permitted:
-                        return (
-                            ATTESTATION_TYPE.NONE,
-                            [],
-                            credential_pub_key,
-                            cred_id,
-                            aaguid
-                        )
-                    else:
-                        raise RegistrationRejectedException('Unsupported algorithm '
-                                                            '({0!s}).'.format(alg))
-                return (
-                    ATTESTATION_TYPE.SELF_ATTESTATION,
-                    [],
-                    credential_pub_key,
-                    cred_id,
-                    aaguid
-                )
         else:
             # Attestation is either none, or unsupported.
             if not none_attestation_permitted:
                 if fmt == ATTESTATION_FORMAT.NONE:
                     raise RegistrationRejectedException('Authenticator attestation is required.')
                 else:
-                    raise RegistrationRejectedException(
-                        'Unsupported authenticator attestation format ({0!s})!'.format(fmt))
+                    raise RegistrationRejectedException('Unsupported authenticator attestation format.')
 
             # Treat as none attestation.
             #
@@ -1234,6 +1194,17 @@ class WebAuthnRegistrationResponse(object):
                 aaguid
             )
 
+    def extract_credential_id(self):
+        # decoded_response = dict(
+        #     [(k, webauthn_b64_decode(v)) for k, v in self.registration_response.items()])
+        attestation_object = self.registration_response["attObj"]
+        decoded_attestation_object = cbor2.loads(webauthn_b64_decode(attestation_object))
+        auth_data = decoded_attestation_object["authData"]
+        attestation_data = auth_data[37:]
+        credential_id_len = struct.unpack('!H', attestation_data[16:18])[0]
+        credential_id = attestation_data[18:18 + credential_id_len]
+        return credential_id
+
     def verify(self, existing_credential_ids=None):
         """
         Verify the WebAuthnRegistrationResponse.
@@ -1245,13 +1216,48 @@ class WebAuthnRegistrationResponse(object):
         :param existing_credential_ids: A list of existing credential ids to check for duplicates.
         :type existing_credential_ids: basestring[]
         :return: The WebAuthnCredential produced by the registration ceremony.
-        :rtype: WebAuthnCredential
+        :rtype: VerifiedRegistration
         """
 
         try:
-            # As described in https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential
-            # In the docs it starts at step 5.
-            #
+            credential_raw_id = self.extract_credential_id()
+            credential_id = bytes_to_base64url(credential_raw_id)
+            response = {
+                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+            }
+
+            raw_registration_response = {
+                # need to make id == base64url(rawID)
+                "rawId": credential_raw_id,
+                "id": credential_id,
+                "response": response
+            }
+
+            verified_registration = verify_registration_response(
+                credential=RegistrationCredential.parse_obj(raw_registration_response),
+                expected_challenge=base64url_to_bytes(self.challenge),
+                expected_rp_id=self.rp_id,
+                expected_origin=self.origin,
+                require_user_verification=self.uv_required
+            )
+            return verified_registration
+            credential = WebAuthnCredential(
+                rp_id=self.rp_id,
+                origin=self.origin,
+                aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
+                credential_id=verified_registration.credential_id,
+                public_key=verified_registration.credential_public_key,
+                sign_count=verified_registration.sign_count,
+                # extract from fmt or attestation_object?
+                attestation_level="none",
+                attestation_cert=None
+            )
+            return credential
+        except InvalidRegistrationResponse as ex:
+            raise RegistrationRejectedException from ex
+
+        try:
             # Step 1.
             #
             # Let JSONtext be the result of running UTF-8 decode on the value of
@@ -1816,7 +1822,7 @@ def _load_cose_public_key(key_bytes):
         y = int(codecs.encode(cose_public_key[COSE_PUBLIC_KEY.Y], 'hex'), 16)
 
         return alg, EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key(backend=default_backend())
-    elif alg in (COSE_ALGORITHM.PS256, COSE_ALGORITHM.RS256, COSE_ALGORITHM.RS1):
+    elif alg in (COSE_ALGORITHM.PS256, COSE_ALGORITHM.RS256):
 
         required_keys = {
             COSE_PUBLIC_KEY.ALG,
@@ -1835,7 +1841,6 @@ def _load_cose_public_key(key_bytes):
 
         return alg, RSAPublicNumbers(e, n).public_key(backend=default_backend())
     else:
-        log.warning('Unsupported webAuthn COSE algorithm: {0!s}'.format(alg))
         raise COSEKeyException('Unsupported algorithm.')
 
 
@@ -2047,7 +2052,5 @@ def _verify_signature(public_key, alg, data, signature):
     elif alg == COSE_ALGORITHM.PS256:
         padding = PSS(mgf=MGF1(SHA256()), salt_length=PSS.MAX_LENGTH)
         public_key.verify(signature, data, padding, SHA256())
-    elif alg == COSE_ALGORITHM.RS1:
-        public_key.verify(signature, data, PKCS1v15(), SHA1())
     else:
         raise NotImplementedError()
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index b52f8a2b3..f43589ef6 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -22,28 +22,37 @@
 #
 
 import binascii
+import logging
 
 from OpenSSL import crypto
 from cryptography import x509
+from webauthn.helpers import bytes_to_base64url
 
 from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
+from privacyidea.lib import _
 from privacyidea.lib.challenge import get_challenges
 from privacyidea.lib.config import get_from_config
 from privacyidea.lib.crypto import geturandom
 from privacyidea.lib.decorators import check_token_locked
-from privacyidea.lib.error import ParameterError, EnrollmentError, PolicyError
+from privacyidea.lib.error import ParameterError, RegistrationError, \
+    PolicyError
+from privacyidea.lib.log import log_with
+from privacyidea.lib.policy import SCOPE, GROUP, ACTION
 from privacyidea.lib.token import get_tokens
 from privacyidea.lib.tokenclass import TokenClass, CLIENTMODE, ROLLOUTSTATE
-from privacyidea.lib.tokens.webauthn import (COSE_ALGORITHM, webauthn_b64_encode, WebAuthnRegistrationResponse,
-                                             ATTESTATION_REQUIREMENT_LEVEL, webauthn_b64_decode,
-                                             WebAuthnMakeCredentialOptions, WebAuthnAssertionOptions, WebAuthnUser,
-                                             WebAuthnAssertionResponse, AuthenticationRejectedException,
-                                             USER_VERIFICATION_LEVEL)
+from privacyidea.lib.tokens.u2f import x509name_to_string
 from privacyidea.lib.tokens.u2ftoken import IMAGES
-from privacyidea.lib.log import log_with
-import logging
-from privacyidea.lib import _
-from privacyidea.lib.policy import SCOPE, GROUP, ACTION
+from privacyidea.lib.tokens.webauthn_new import (COSE_ALGORITHM,
+                                                 webauthn_b64_encode,
+                                                 WebAuthnRegistrationResponse,
+                                                 ATTESTATION_REQUIREMENT_LEVEL,
+                                                 webauthn_b64_decode,
+                                                 WebAuthnMakeCredentialOptions,
+                                                 WebAuthnAssertionOptions,
+                                                 WebAuthnUser,
+                                                 WebAuthnAssertionResponse,
+                                                 AuthenticationRejectedException,
+                                                 USER_VERIFICATION_LEVEL)
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import hexlify_and_unicode, is_true
 
@@ -77,69 +86,44 @@ The enrollment/registering can be completely performed within privacyIDEA.
 But if you want to enroll the WebAuthn token via the REST API you need to do
 it in two steps:
 
-**Step 1**
+Step 1
+~~~~~~
 
 .. sourcecode:: http
 
     POST /token/init HTTP/1.1
-    Host: <privacyIDEA server>
+    Host: example.com
     Accept: application/json
-
+    
     type=webauthn
-    user=<username>
+    
+This step returns a nonce, a relying party (containing a name and an ID
+generated from your domain), and a serial number, along with a transaction ID,
+and a message to display to the user. It will also pass some additional options
+regarding timeout, which authenticators are acceptable, and what key types are
+acceptable to the server.
 
-The request returns:
+Step 2
+~~~~~~
 
 .. sourcecode:: http
 
-    HTTP/1.1 200 OK
-    Content-Type: application/json
-
-    {
-        "detail": {
-            "serial": "<serial number>",
-            "webAuthnRegisterRequest": {
-                "attestation": "direct",
-                "authenticatorSelection": {
-                    "userVerification": "preferred"
-                },
-                "displayName": "<user.resolver@realm>",
-                "message": "Please confirm with your WebAuthn token",
-                "name": "<username>",
-                "nonce": "<nonce>",
-                "pubKeyCredAlgorithms": [
-                    {
-                        "alg": -7,
-                        "type": "public-key"
-                    },
-                    {
-                        "alg": -37,
-                        "type": "public-key"
-                    }
-                ],
-                "relyingParty": {
-                    "id": "<relying party ID>",
-                    "name": "<relying party name>"
-                },
-                "serialNumber": "<serial number>",
-                "timeout": 60000,
-                "transaction_id": "<transaction ID>"
-            }
-        },
-        "result": {
-            "status": true,
-            "value": true
-        },
-        "version": "<privacyIDEA version>"
-    }
+    POST /token/init HTTP/1.1
+    Host: example.com
+    Accept: application/json
+    
+    type=webauthn
+    transaction_id=<transaction_id>
+    description=<description>
+    clientdata=<clientDataJSON>
+    regdata=<attestationObject>
+    registrationclientextensions=<registrationClientExtensions>
 
-This step returns a *webAuthnRegisterRequest* which contains a nonce, a relying party (containing a
-name and an ID generated from your domain), a serial number along with a transaction ID
-and a message to display to the user. It will also contain some additional options
-regarding timeout, which authenticators are acceptable, and what key types are
-acceptable to the server.
+*clientDataJSON* and *attestationObject* are the values returned by the
+WebAuthn authenticator. *description* is an optional description string for 
+the new token.
 
-With the received data You need to call the javascript function
+You need to call the javascript function
 
 .. sourcecode:: javascript
 
@@ -153,7 +137,16 @@ With the received data You need to call the javascript function
                 name: <name>,
                 displayName: <displayName>
             },
-            pubKeyCredParams: <pubKeyCredAlgorithms>,
+            pubKeyCredParams: [
+                {
+                    alg: <preferredAlgorithm>,
+                    type: "public-key"
+                },
+                {
+                    alg: <alternativeAlgorithm>,
+                    type: "public-key"
+                }
+            ],
             authenticatorSelection: <authenticatorSelection>,
             timeout: <timeout>,
             attestation: <attestation>,
@@ -164,11 +157,11 @@ With the received data You need to call the javascript function
         .then(function(credential) { <responseHandler> })
         .catch(function(error) { <errorHandler> });
 
-Here *nonce*, *relyingParty*, *serialNumber*, *pubKeyCredAlgorithms*,
-*authenticatorSelection*, *timeout*, *attestation*,
+Here *nonce*, *relyingParty*, *serialNumber*, *preferredAlgorithm*,
+*alternativeAlgorithm*, *authenticatorSelection*, *timeout*, *attestation*,
 *authenticatorSelectionList*, *name*, and *displayName* are the values
 provided by the server in the *webAuthnRegisterRequest* field in the response
-from the first step. *authenticatorSelection*,
+from the first step. *alternativeAlgorithm*, *authenticatorSelection*,
 *timeout*, *attestation*, and *authenticatorSelectionList* are optional. If
 *attestation* is not provided, the client should default to `direct`
 attestation. If *timeout* is not provided, it may be omitted, or a sensible
@@ -185,38 +178,18 @@ company-provided token.
 
 The *responseHandler* needs to then send the *clientDataJSON*,
 *attestationObject*, and *registrationClientExtensions* contained in the
-*response* field of the *credential* back to the server. If
+*response* field of the *credential* (2. step) back to the server. If
 enrollment succeeds, the server will send a response with a
 *webAuthnRegisterResponse* field, containing a *subject* field with the
 description of the newly created token.
 
-
-**Step 2**
-
-.. sourcecode:: http
-
-    POST /token/init HTTP/1.1
-    Host: <privacyIDEA server>
-    Accept: application/json
-
-    type=webauthn
-    transaction_id=<transaction_id>
-    description=<description>
-    clientdata=<clientDataJSON>
-    regdata=<attestationObject>
-    registrationclientextensions=<registrationClientExtensions>
-
-The values *clientDataJSON* and *attestationObject* are returned by the
-WebAuthn authenticator. *description* is an optional description string for
-the new token.
-
 The server expects the *clientDataJSON* and *attestationObject* encoded as
 web-safe base64 as defined by the WebAuthn standard. This encoding is similar
 to standard base64, but '-' and '_' should be used in the alphabet instead of
 '+' and '/', respectively, and any padding should be omitted.
 
 The *registrationClientExtensions* are optional and should simply be omitted,
-if the client does not provide them. If the *registrationClientExtensions* are
+if the client does not provide them. It the *registrationClientExtensions* are
 available, they must be encoded as a utf-8 JSON string, then sent to the server
 as web-safe base64.
 
@@ -243,62 +216,53 @@ Get the challenge (using /validate/check)
 The /validate/check endpoint can be used to trigger a challenge using the PIN
 for the token (without requiring any special permissions).
 
-**Request:**
+**Request**
 
 .. sourcecode:: http
 
     POST /validate/check HTTP/1.1
-    Host: <privacyIDEA server>
+    Host: example.com
     Accept: application/json
-
+    
     user=<username>
     pass=<password>
-
-**Response:**
+    
+**Response**
 
 .. sourcecode:: http
 
     HTTP/1.1 200 OK
     Content-Type: application/json
-
+    
     {
         "detail": {
             "attributes": {
                 "hideResponseInput": true,
-                "img": "<image URL>",
+                "img": <imageUrl>,
                 "webAuthnSignRequest": {
-                    "allowCredentials": [
-                        {
-                            "id": "<credential ID>",
-                            "transports": [
-                                "<allowed transports>"
-                            ],
-                            "type": "<credential type>"
-                        }
-                    ],
-                    "challenge": "<nonce>",
-                    "rpId": "<relying party ID>",
-                    "timeout": 60000,
-                    "userVerification": "<user verification requirement>"
+                    "challenge": <nonce>,
+                    "allowCredentials": [{
+                        "id": <credentialId>,
+                        "type": <credentialType>,
+                        "transports": <allowedTransports>,
+                    }],
+                    "rpId": <relyingPartyId>,
+                    "userVerification": <userVerificationRequirement>,
+                    "timeout": <timeout>
                 }
             },
-            "client_mode": "webauthn",
             "message": "Please confirm with your WebAuthn token",
-            "serial": "<token serial>",
-            "transaction_id": "<transaction ID>",
-            "type": "webauthn"
+            "transaction_id": <transactionId>
         },
         "id": 1,
         "jsonrpc": "2.0",
         "result": {
-            "authentication": "CHALLENGE",
             "status": true,
             "value": false
         },
-        "version": "<privacyIDEA version>"
+        "versionnumber": <privacyIDEAversion>
     }
 
-
 Get the challenge (using /validate/triggerchallenge)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -310,13 +274,13 @@ using a service account (without requiring the PIN for the token).
 .. sourcecode:: http
 
     POST /validate/triggerchallenge HTTP/1.1
-    Host: <privacyIDEA server>
+    Host: example.com
     Accept: application/json
     PI-Authorization: <authToken>
-
+    
     user=<username>
     serial=<tokenSerial>
-
+    
 Providing the *tokenSerial* is optional. If just a user is provided, a
 challenge will be triggered for every challenge response token the user has.
 
@@ -326,24 +290,23 @@ challenge will be triggered for every challenge response token the user has.
 
     HTTP/1.1 200 OK
     Content-Type: application/json
-
+    
+    
     {
         "detail": {
             "attributes": {
                 "hideResponseInput": true,
-                "img": "<image URL>",
+                "img": <imageUrl>,
                 "webAuthnSignRequest": {
-                    "challenge": "<nonce>",
+                    "challenge": <nonce>,
                     "allowCredentials": [{
-                        "id": "<credential ID>",
-                        "transports": [
-                            "<allowed transports>"
-                        ],
-                        "type": "<credential type>",
+                        "id": <credentialId>,
+                        "type": <credentialType>,
+                        "transports": <allowedTransports>,
                     }],
-                    "rpId": "<relying party ID>",
-                    "userVerification": "<user verification requirement>",
-                    "timeout": 60000
+                    "rpId": <relyingPartyId>,
+                    "userVerification": <userVerificationRequirement>,
+                    "timeout": <timeout>
                 }
             },
             "message": "Please confirm with your WebAuthn token",
@@ -351,29 +314,28 @@ challenge will be triggered for every challenge response token the user has.
             "multi_challenge": [{
                 "attributes": {
                     "hideResponseInput": true,
-                    "img": "<image URL>",
+                    "img": <imageUrl>,
                     "webAuthnSignRequest": {
-                        "challenge": "<nonce>",
+                        "challenge": <nonce>,
                         "allowCredentials": [{
-                            "id": "<credential ID>",
-                            "transports": [
-                                "<allowedTransports>"
-                            ],
-                            "type": "<credential type>",
+                            "id": <credentialId>,
+                            "type": <credentialType>,
+                            "transports": <allowedTransports>,
                         }],
-                        "rpId": "<relying party ID>",
-                        "userVerification": "<user verification requirement>",
-                        "timeout": 60000
+                        "rpId": <relyingPartyId>,
+                        "userVerification": <userVerificationRequirement>,
+                        "timeout": <timeout>
                     }
                 },
                 "message": "Please confirm with your WebAuthn token",
-                "serial": "<token serial>",
-                "transaction_id": "<transaction ID>",
+                "serial": <tokenSerial>,
+                "transaction_id": <transactionId>,
                 "type": "webauthn"
             }],
-            "serial": "<token serial>",
-            "transaction_id": "<transaction ID>",
-            "transaction_ids": ["<transaction IDs>"],
+            "serial": <tokenSerial>,
+            "threadid": <threadId>,
+            "transaction_id": <transactionId>,
+            "transaction_ids": [<transactionId>],
             "type": "webauthn"
         },
         "id": 1,
@@ -382,15 +344,15 @@ challenge will be triggered for every challenge response token the user has.
             "status": true,
             "value": 1
         },
-        "version": "<privacyIDEA version>"
+        "versionnumber": <privacyIDEAversion>
     }
-
+    
 Send the Response
 ~~~~~~~~~~~~~~~~~
 
 The application now needs to call the javascript function
-*navigator.credentials.get* with the *publicKeyCredentialRequestOptions* built
-using the *nonce*, *credentialId*, *allowedTransports*, *userVerificationRequirement*
+*navigator.credentials.get* with *publicKeyCredentialRequestOptions* built using
+the *nonce*, *credentialId*, *allowedTransports*, *userVerificationRequirement*
 and *timeout* from the server.  The timeout is optional and may be omitted, if
 not provided, the client may also pick a sensible default. Please note that the
 nonce will be a binary, encoded using the web-safe base64 algorithm specified by
@@ -440,7 +402,7 @@ native encoding of the language (usually utf-16).
     POST /validate/check HTTP/1.1
     Host: example.com
     Accept: application/json
-
+    
     user=<user>
     pass=
     transaction_id=<transaction_id>
@@ -464,20 +426,28 @@ DEFAULT_ALLOWED_TRANSPORTS = "usb ble nfc internal"
 DEFAULT_TIMEOUT = 60
 DEFAULT_USER_VERIFICATION_REQUIREMENT = 'preferred'
 DEFAULT_AUTHENTICATOR_ATTACHMENT = 'either'
-DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = ['ecdsa', 'rsassa-pss']
+DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = 'ecdsa_preferred'
 DEFAULT_AUTHENTICATOR_ATTESTATION_LEVEL = 'untrusted'
 DEFAULT_AUTHENTICATOR_ATTESTATION_FORM = 'direct'
 DEFAULT_CHALLENGE_TEXT_AUTH = _(u'Please confirm with your WebAuthn token ({0!s})')
 DEFAULT_CHALLENGE_TEXT_ENROLL = _(u'Please confirm with your WebAuthn token')
 
-PUBLIC_KEY_CREDENTIAL_ALGORITHMS = {
-    'ecdsa': COSE_ALGORITHM.ES256,
-    'rsassa-pss': COSE_ALGORITHM.PS256,
-    'rsassa-pkcs1v1_5': COSE_ALGORITHM.RS256
+PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE_OPTIONS = {
+    'ecdsa_preferred': [
+        COSE_ALGORITHM.ES256,
+        COSE_ALGORITHM.PS256
+    ],
+    'ecdsa_only': [
+        COSE_ALGORITHM.ES256
+    ],
+    'rsassa-pss_preferred': [
+        COSE_ALGORITHM.PS256,
+        COSE_ALGORITHM.ES256
+    ],
+    'rsassa-pss_only': [
+        COSE_ALGORITHM.PS256
+    ]
 }
-# since in Python < 3.7 the insert order of a dictionary is not guaranteed, we
-# need a list to define the proper order
-PUBKEY_CRED_ALGORITHMS_ORDER = ['ecdsa', 'rsassa-pss', 'rsassa-pkcs1v1_5']
 
 log = logging.getLogger(__name__)
 optional = True
@@ -512,7 +482,7 @@ class WEBAUTHNACTION(object):
     AUTHENTICATOR_ATTACHMENT = 'webauthn_authenticator_attachment'
     AUTHENTICATOR_SELECTION_LIST = 'webauthn_authenticator_selection_list'
     USER_VERIFICATION_REQUIREMENT = 'webauthn_user_verification_requirement'
-    PUBLIC_KEY_CREDENTIAL_ALGORITHMS = 'webauthn_public_key_credential_algorithms'
+    PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = 'webauthn_public_key_credential_algorithm_preference'
     AUTHENTICATOR_ATTESTATION_FORM = 'webauthn_authenticator_attestation_form'
     AUTHENTICATOR_ATTESTATION_LEVEL = 'webauthn_authenticator_attestation_level'
     REQ = 'webauthn_req'
@@ -533,6 +503,12 @@ class WEBAUTHNINFO(object):
     ATTESTATION_SUBJECT = "attestation_subject"
     RELYING_PARTY_ID = "relying_party_id"
     RELYING_PARTY_NAME = "relying_party_name"
+    FORMAT = "format"
+    CREDENTIAL_TYPE = "credential_type"
+    USER_VERIFIED = "user_verified"
+    ATTESTATION_OBJECT = "attestation_object"
+    CREDENTIAL_DEVICE_TYPE = "credential_device_type"
+    CREDENTIAL_BACKED_UP = "credential_backed_up"
 
 
 class WEBAUTHNGROUP(object):
@@ -695,15 +671,17 @@ class WebAuthnTokenClass(TokenClass):
                             "discouraged"
                         ]
                     },
-                    WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: {
+                    WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: {
                         'type': 'str',
-                        'desc': _("Which algorithm are available to use for creating public key "
-                                  "credentials for WebAuthn tokens. (Default: [{0!s}], Order: "
-                                  "[{1!s}]".format(', '.join(DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE),
-                                                   ', '.join(PUBKEY_CRED_ALGORITHMS_ORDER))),
+                        'desc': _("Which algorithm to use for creating public key credentials for WebAuthn tokens. "
+                                  "Default: ecdsa_preferred"),
                         'group': WEBAUTHNGROUP.WEBAUTHN,
-                        'multiple': True,
-                        'value': list(PUBLIC_KEY_CREDENTIAL_ALGORITHMS.keys())
+                        'value': [
+                            "ecdsa_preferred",
+                            "ecdsa_only",
+                            "rsassa-pss_preferred",
+                            "rsassa-pss_only"
+                        ]
                     },
                     WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: {
                         'type': 'str',
@@ -874,7 +852,7 @@ class WebAuthnTokenClass(TokenClass):
 
             # Since we are still enrolling the token, there should be exactly one challenge.
             if not len(challengeobject_list):
-                raise EnrollmentError(
+                raise RegistrationError(
                     "The enrollment challenge does not exist or has timed out for {0!s}".format(serial))
             challengeobject = challengeobject_list[0]
             challenge = binascii.unhexlify(challengeobject.challenge)
@@ -883,56 +861,79 @@ class WebAuthnTokenClass(TokenClass):
             #
             # All data is parsed and verified. If any errors occur an exception
             # will be raised.
-            try:
-                webauthn_credential = WebAuthnRegistrationResponse(
-                    rp_id=rp_id,
-                    origin=http_origin,
-                    registration_response={
-                        'clientData': client_data,
-                        'attObj': reg_data,
-                        'registrationClientExtensions':
-                            webauthn_b64_decode(registration_client_extensions)
+            web_authn_credential = WebAuthnRegistrationResponse(
+                rp_id=rp_id,
+                origin=http_origin,
+                registration_response={
+                    'clientData': client_data,
+                    'attObj': reg_data,
+                    'registrationClientExtensions':
+                        webauthn_b64_decode(registration_client_extensions)
                             if registration_client_extensions
                             else None
-                    },
-                    challenge=webauthn_b64_encode(challenge),
-                    attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[attestation_level],
-                    trust_anchor_dir=get_from_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR),
-                    uv_required=uv_req == USER_VERIFICATION_LEVEL.REQUIRED
-                ).verify([
-                    # TODO: this might get slow when a lot of webauthn tokens are registered
-                    token.decrypt_otpkey() for token in get_tokens(tokentype=self.type) if token.get_serial() != self.get_serial()
-                ])
-            except Exception as e:
-                log.warning('Enrollment of {0!s} token failed: '
-                            '{1!s}!'.format(self.get_class_type(), e))
-                raise EnrollmentError("Could not enroll {0!s} token!".format(self.get_class_type()))
-
-            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(webauthn_credential.credential_id)))
-            self.set_otp_count(webauthn_credential.sign_count)
+                },
+                challenge=webauthn_b64_encode(challenge),
+                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[attestation_level],
+                trust_anchor_dir=get_from_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR),
+                uv_required=uv_req == USER_VERIFICATION_LEVEL.REQUIRED
+            ).verify([
+                token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
+            ])
+
+            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id))))
+            self.set_otp_count(web_authn_credential.sign_count)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(webauthn_credential.public_key)))
+                               hexlify_and_unicode(webauthn_b64_decode(
+                                   bytes_to_base64url(
+                                       web_authn_credential.credential_public_key))))
+            self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
+                               hexlify_and_unicode(
+                                   web_authn_credential.aaguid))
+            self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
+                               web_authn_credential.fmt)
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
+                               web_authn_credential.credential_type)
+            self.add_tokeninfo(WEBAUTHNINFO.USER_VERIFIED,
+                               web_authn_credential.user_verified)
+            self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_OBJECT,
+                               bytes_to_base64url(web_authn_credential.attestation_object))
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_DEVICE_TYPE,
+                               web_authn_credential.credential_device_type)
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
+                               web_authn_credential.credential_device_type)
+
+            '''
+            self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
+                               hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.public_key))))
             self.add_tokeninfo(WEBAUTHNINFO.ORIGIN,
-                               webauthn_credential.origin)
+                               web_authn_credential.origin)
             self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_LEVEL,
-                               webauthn_credential.attestation_level)
+                               web_authn_credential.attestation_level)
 
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
-                               hexlify_and_unicode(webauthn_credential.aaguid))
+                               hexlify_and_unicode(web_authn_credential.aaguid))
 
             # Add attestation info.
-            if webauthn_credential.attestation_cert:
-                # attestation_cert is of type cryptography.x509.Certificate.
+            if web_authn_credential.attestation_cert:
+                # attestation_cert is of type X509. If you get warnings from your IDE
+                # here, it is because your IDE mistakenly assumes it to be of type PKey,
+                # due to a bug in pyOpenSSL 18.0.0. This bug is – however – purely
+                # cosmetic (a wrongly hinted return type in X509.from_cryptography()),
+                # and can be safely ignored.
+                #
+                # See also:
+                # https://github.com/pyca/pyopenssl/commit/4121e2555d07bbba501ac237408a0eea1b41f467
+                attestation_cert = crypto.X509.from_cryptography(web_authn_credential.attestation_cert)
                 self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER,
-                                   webauthn_credential.attestation_cert.issuer.rfc4514_string())
+                                   x509name_to_string(attestation_cert.get_issuer()))
                 self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT,
-                                   webauthn_credential.attestation_cert.subject.rfc4514_string())
+                                   x509name_to_string(attestation_cert.get_subject()))
                 self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL,
-                                   webauthn_credential.attestation_cert.serial_number)
+                                   attestation_cert.get_serial_number())
 
-                cn = webauthn_credential.attestation_cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
+                cn = web_authn_credential.attestation_cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
                 automatic_description = cn[0].value if len(cn) else None
-
+            '''
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
             if not self.token.description:
@@ -1019,7 +1020,7 @@ class WebAuthnTokenClass(TokenClass):
                                            WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT,
                                            required),
                 public_key_credential_algorithms=getParam(params,
-                                                          WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS,
+                                                          WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE,
                                                           required),
                 authenticator_attachment=getParam(params,
                                                   WEBAUTHNACTION.AUTHENTICATOR_ATTACHMENT,
@@ -1036,10 +1037,13 @@ class WebAuthnTokenClass(TokenClass):
                 "nonce": public_key_credential_creation_options["challenge"],
                 "relyingParty": public_key_credential_creation_options["rp"],
                 "serialNumber": public_key_credential_creation_options["user"]["id"],
-                "pubKeyCredAlgorithms": public_key_credential_creation_options["pubKeyCredParams"],
+                "preferredAlgorithm": public_key_credential_creation_options["pubKeyCredParams"][0],
                 "name": public_key_credential_creation_options["user"]["name"],
                 "displayName": public_key_credential_creation_options["user"]["displayName"]
             }
+            if len(public_key_credential_creation_options.get("pubKeyCredParams")) > 1:
+                response_detail["webAuthnRegisterRequest"]["alternativeAlgorithm"] \
+                    = public_key_credential_creation_options["pubKeyCredParams"][1]
             if public_key_credential_creation_options.get("authenticatorSelection"):
                 response_detail["webAuthnRegisterRequest"]["authenticatorSelection"] \
                     = public_key_credential_creation_options["authenticatorSelection"]
@@ -1182,7 +1186,7 @@ class WebAuthnTokenClass(TokenClass):
                       "image": user.icon_url}
 
         return True, message, db_challenge.transaction_id, reply_dict
-
+    
     @check_token_locked
     def check_otp(self, otpval, counter=None, window=None, options=None):
         """
@@ -1266,10 +1270,9 @@ class WebAuthnTokenClass(TokenClass):
                     getParam(options, WEBAUTHNACTION.REQ, optional)
             ):
                 log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate "
-                    "due to policy restriction {1!s}".format(self.token.serial, WEBAUTHNACTION.REQ))
-                raise PolicyError("The WebAuthn token is not allowed to "
-                                  "authenticate due to a policy restriction.")
+                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy restriction {1!s}"
+                        .format(self.token.serial, WEBAUTHNACTION.REQ))
+                raise PolicyError("The WebAuthn token is not allowed to authenticate due to a policy restriction.")
 
             # Now we need to check, if a whitelist for AAGUIDs exists, and if
             # so, if this device is whitelisted. If not, we again raise a
@@ -1277,10 +1280,9 @@ class WebAuthnTokenClass(TokenClass):
             allowed_aaguids = getParam(options, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST, optional)
             if allowed_aaguids and self.get_tokeninfo(WEBAUTHNINFO.AAGUID) not in allowed_aaguids:
                 log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy "
-                    "restriction {1!s}".format(self.token.serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-                raise PolicyError("The WebAuthn token is not allowed to "
-                                  "authenticate due to a policy restriction.")
+                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy restriction {1!s}"
+                        .format(self.token.serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
+                raise PolicyError("The WebAuthn token is not allowed to authenticate due to a policy restriction.")
 
             # All clear? Nice!
             return self.get_otp_count()
-- 
GitLab


From 1a1003ec4aa2f014773eb5af7b4156ca9d5d9e21 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 10 Oct 2022 17:05:04 +0200
Subject: [PATCH 14/26] feat: removed_hexlification_of_keys

---
 privacyidea/lib/tokens/webauthntoken.py | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index f43589ef6..d8b8eec18 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -785,7 +785,7 @@ class WebAuthnTokenClass(TokenClass):
             user_display_name=str(user),
             icon_url=IMAGES.get(self.token.description.lower().split()[0], "") if self.token.description else "",
             credential_id=self.decrypt_otpkey(),
-            public_key=webauthn_b64_encode(binascii.unhexlify(self.get_tokeninfo(WEBAUTHNINFO.PUB_KEY))),
+            public_key=self.get_tokeninfo(WEBAUTHNINFO.PUB_KEY),
             sign_count=self.get_otp_count(),
             rp_id=self.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID)
         )
@@ -804,7 +804,7 @@ class WebAuthnTokenClass(TokenClass):
         :rtype: basestring
         """
 
-        return webauthn_b64_encode(binascii.unhexlify(self.token.get_otpkey().getKey()))
+        return webauthn_b64_encode(self.token.get_otpkey().getKey())
 
     def update(self, param, reset_failcount=True):
         """
@@ -880,12 +880,11 @@ class WebAuthnTokenClass(TokenClass):
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
 
-            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id))))
+            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id)))
             self.set_otp_count(web_authn_credential.sign_count)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(
                                    bytes_to_base64url(
-                                       web_authn_credential.credential_public_key))))
+                                       web_authn_credential.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
                                hexlify_and_unicode(
                                    web_authn_credential.aaguid))
-- 
GitLab


From 88dac33a39ede5e18f17a1d10d48b0b3ca54919a Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 09:41:23 +0200
Subject: [PATCH 15/26] feat:
 replaced_user_authentication_with_py_webauthn_method

---
 privacyidea/lib/tokens/webauthntoken.py | 39 +++++++++++++++++++++++--
 1 file changed, 37 insertions(+), 2 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index d8b8eec18..995ae2ef8 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -26,7 +26,10 @@ import logging
 
 from OpenSSL import crypto
 from cryptography import x509
+from webauthn import verify_authentication_response, base64url_to_bytes
 from webauthn.helpers import bytes_to_base64url
+from webauthn.helpers.exceptions import InvalidAuthenticationResponse
+from webauthn.helpers.structs import AuthenticationCredential
 
 from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
 from privacyidea.lib import _
@@ -1207,7 +1210,6 @@ class WebAuthnTokenClass(TokenClass):
         :return: A numerical value where values larger than zero indicate success.
         :rtype: int
         """
-
         if is_webauthn_assertion_response(options) and getParam(options, "challenge", optional):
             credential_id = getParam(options, "credentialid", required)
             authenticator_data = getParam(options, "authenticatordata", required)
@@ -1215,12 +1217,45 @@ class WebAuthnTokenClass(TokenClass):
             signature_data = getParam(options, "signaturedata", required)
             user_handle = getParam(options, "userhandle", optional)
             assertion_client_extensions = getParam(options, "assertionclientextensions", optional)
-
             try:
                 user = self._get_webauthn_user(getParam(options, "user", required))
             except ParameterError:
                 raise ValueError("When performing WebAuthn authorization, options must contain user")
 
+            try:
+                auth_id = credential_id
+                auth_raw_id = base64url_to_bytes(auth_id)
+
+                response = {
+                    "authenticatorData": webauthn_b64_decode(authenticator_data),
+                    "clientDataJSON": webauthn_b64_decode(client_data),
+                    "signature": webauthn_b64_decode(signature_data),
+                    "userHandle": user_handle
+                }
+                raw_auth_response = {
+                    "rawId": auth_raw_id,
+                    "id": auth_id,
+                    "response": response
+                }
+
+                verified_authentication = verify_authentication_response(
+                    credential=AuthenticationCredential.parse_obj(
+                        raw_auth_response),
+                    expected_challenge=binascii.unhexlify(options["challenge"]),
+                    expected_rp_id=user.rp_id,
+                    expected_origin=options["HTTP_ORIGIN"],
+                    credential_public_key=user.public_key,
+                    credential_current_sign_count=user.sign_count,
+                    require_user_verification=options.get(
+                        WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, False)
+                )
+                sign_count = verified_authentication.new_sign_count
+                self.set_otp_count(sign_count)
+                return sign_count
+
+            except InvalidAuthenticationResponse as ex:
+                raise AuthenticationRejectedException from ex
+
             uv_req = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
 
             challenge = binascii.unhexlify(getParam(options, "challenge", required))
-- 
GitLab


From f88caed6b140bfc6e9fbae07726079a6a87e51c4 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 11:07:51 +0200
Subject: [PATCH 16/26] refactor: removed_dead_code

---
 privacyidea/lib/tokens/webauthn_new.py  | 234 ------------------------
 privacyidea/lib/tokens/webauthntoken.py | 109 +----------
 2 files changed, 4 insertions(+), 339 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthn_new.py b/privacyidea/lib/tokens/webauthn_new.py
index 36b4455f4..41b50bdcc 100644
--- a/privacyidea/lib/tokens/webauthn_new.py
+++ b/privacyidea/lib/tokens/webauthn_new.py
@@ -1257,240 +1257,6 @@ class WebAuthnRegistrationResponse(object):
         except InvalidRegistrationResponse as ex:
             raise RegistrationRejectedException from ex
 
-        try:
-            # Step 1.
-            #
-            # Let JSONtext be the result of running UTF-8 decode on the value of
-            # response.clientDataJSON
-            json_text = self.registration_response.get('clientData', '')
-
-            # Step 2.
-            #
-            # Let C, the client data claimed as collected during the credential
-            # creation, be the result of running an implementation-specific JSON
-            # parser on JSONtext.
-            decoded_cd = webauthn_b64_decode(json_text)
-            c = json.loads(to_unicode(decoded_cd))
-
-            # Step 3.
-            #
-            # Verify that the value of C.type is webauthn.create.
-            if not _verify_type(c.get('type'), CLIENT_DATA_TYPE.CREATE):
-                raise RegistrationRejectedException('Invalid type.')
-
-            # Step 4.
-            #
-            # Verify that the value of C.challenge matches the challenge that was sent
-            # to the authenticator in the create() call.
-            if not _verify_challenge(c.get('challenge'), self.challenge):
-                raise RegistrationRejectedException('Unable to verify challenge.')
-
-            # Step 5.
-            #
-            # Verify that the value of C.origin matches the Relying Party's origin.
-            if not _verify_origin(c, self.origin):
-                raise RegistrationRejectedException('Unable to verify origin.')
-
-            # Step 6.
-            #
-            # Verify that the value of C.tokenBinding.status matches the state of
-            # Token Binding for the TLS connection over which the assertion was
-            # obtained. If Token Binding was used on that TLS connection, also verify
-            # that C.tokenBinding.id matches the base64url encoding of the Token
-            # Binding ID for the connection.
-
-            # Chrome does not currently supply token binding in the clientDataJSON
-            # if not _verify_token_binding_id(c):
-            #     raise RegistrationRejectedException('Unable to verify token binding ID.')
-
-            # Step 7.
-            #
-            # Compute the hash of response.clientDataJSON using SHA-256.
-            client_data_hash = _get_client_data_hash(decoded_cd)
-
-            # Step 8.
-            #
-            # Perform CBOR decoding on the attestationObject field of
-            # the AuthenticatorAttestationResponse structure to obtain
-            # the attestation statement format fmt, the authenticator
-            # data authData, and the attestation statement attStmt.
-            att_obj = self.parse_attestation_object(self.registration_response.get('attObj'))
-            att_stmt = att_obj.get('attStmt')
-            auth_data = att_obj.get('authData')
-            fmt = att_obj.get('fmt')
-            if not auth_data or len(auth_data) < 37:
-                raise RegistrationRejectedException('Auth data must be at least 37 bytes.')
-
-            # Step 9.
-            #
-            # Verify that the RP ID hash in authData is indeed the
-            # SHA-256 hash of the RP ID expected by the RP.
-            if not _verify_rp_id_hash(_get_auth_data_rp_id_hash(auth_data), self.rp_id):
-                raise RegistrationRejectedException('Unable to verify RP ID hash.')
-
-            # Step 10.
-            #
-            # Verify that the User Present bit of the flags in authData
-            # is set.
-            if not AuthenticatorDataFlags(auth_data).user_present:
-                raise RegistrationRejectedException('Malformed request received.')
-
-            # Step 11.
-            #
-            # If user verification is required for this registration, verify
-            # that the User Verified bit of the flags in authData is set.
-            if self.uv_required and not AuthenticatorDataFlags(auth_data).user_verified:
-                raise RegistrationRejectedException('Malformed request received.')
-
-            # Step 12.
-            #
-            # Verify that the values of the client extension outputs in
-            # clientExtensionResults and the authenticator extension outputs
-            # in the extensions in authData are as expected, considering the
-            # client extension input values that were given as the extensions
-            # option in the create() call. In particular, any extension
-            # identifier values in the clientExtensionResults and the extensions
-            # in authData MUST be also be present as extension identifier values
-            # in the extensions member of options, i.e., no extensions are
-            # present that were not requested. In the general case, the meaning
-            # of "are as expected" is specific to the Relying Party and which
-            # extensions are in use.
-            if not _verify_authenticator_extensions(auth_data, self.expected_registration_authenticator_extensions):
-                raise RegistrationRejectedException('Unable to verify authenticator extensions.')
-            if not _verify_client_extensions(
-                self.registration_response.get('registrationClientExtensions'),
-                self.expected_registration_client_extensions
-            ):
-                raise RegistrationRejectedException('Unable to verify client extensions.')
-
-            # Step 13.
-            #
-            # Determine the attestation statement format by performing
-            # a USASCII case-sensitive match on fmt against the set of
-            # supported WebAuthn Attestation Statement Format Identifier
-            # values. The up-to-date list of registered WebAuthn
-            # Attestation Statement Format Identifier values is maintained
-            # in the IANA registry of the same name.
-            if not _verify_attestation_statement_format(fmt):
-                raise RegistrationRejectedException('Unable to verify attestation statement format.')
-
-            # Step 14.
-            #
-            # Verify that attStmt is a correct attestation statement, conveying
-            # a valid attestation signature, by using the attestation statement
-            # format fmt's verification procedure given attStmt, authData and
-            # the hash of the serialized client data computed in step 7.
-            (
-                attestation_type,
-                trust_path,
-                credential_public_key,
-                cred_id,
-                aaguid
-            ) = self.verify_attestation_statement(
-                fmt,
-                att_stmt,
-                auth_data,
-                client_data_hash,
-                self.none_attestation_permitted
-            )
-            b64_cred_id = webauthn_b64_encode(cred_id)
-
-            # Step 15.
-            #
-            # If validation is successful, obtain a list of acceptable trust
-            # anchors (attestation root certificates or ECDAA-Issuer public
-            # keys) for that attestation type and attestation statement format
-            # fmt, from a trusted source or from policy. For example, the FIDO
-            # Metadata Service [FIDOMetadataService] provides one way to obtain
-            # such information, using the aaguid in the attestedCredentialData
-            # in authData.
-            trust_anchors = _get_trust_anchors(attestation_type, fmt, self.trust_anchor_dir) \
-                if self.trust_anchor_dir \
-                else None
-            if not trust_anchors and self.trusted_attestation_cert_required:
-                raise RegistrationRejectedException('No trust anchors available to verify attestation certificate.')
-
-            # Step 16.
-            #
-            # Assess the attestation trustworthiness using the outputs of the
-            # verification procedure in step 14, as follows:
-            #
-            #     * If self attestation was used, check if self attestation is
-            #       acceptable under Relying Party policy.
-            #     * If ECDAA was used, verify that the identifier of the
-            #       ECDAA-Issuer public key used is included in the set of
-            #       acceptable trust anchors obtained in step 15.
-            #     * Otherwise, use the X.509 certificates returned by the
-            #       verification procedure to verify that the attestation
-            #       public key correctly chains up to an acceptable root
-            #       certificate.
-            if attestation_type == ATTESTATION_TYPE.SELF_ATTESTATION and not self.self_attestation_permitted:
-                raise RegistrationRejectedException('Self attestation is not permitted.')
-            is_trusted_attestation_cert = ((attestation_type == ATTESTATION_TYPE.BASIC
-                                            and _is_trusted_x509_attestation_cert(trust_path, trust_anchors))
-                                           or (attestation_type == ATTESTATION_TYPE.ECDAA
-                                               and _is_trusted_ecdaa_attestation_certificate(None, trust_anchors)))
-            is_signed_attestation_cert = attestation_type in SUPPORTED_ATTESTATION_TYPES
-
-            if is_trusted_attestation_cert:
-                attestation_level = ATTESTATION_LEVEL.TRUSTED
-            elif is_signed_attestation_cert:
-                attestation_level = ATTESTATION_LEVEL.UNTRUSTED
-            else:
-                attestation_level = ATTESTATION_LEVEL.NONE
-
-            # Step 17.
-            #
-            # Check that the credentialId is not yet registered to any other user.
-            # If registration is requested for a credential that is already registered
-            # to a different user, the Relying Party SHOULD fail this registration
-            # ceremony, or it MAY decide to accept the registration, e.g. while deleting
-            # the older registration.
-            if existing_credential_ids and b64_cred_id in existing_credential_ids:
-                raise RegistrationRejectedException('Credential already exists.')
-
-            # Step 18.
-            #
-            # If the attestation statement attStmt verified successfully and is
-            # found to be trustworthy, then register the new credential with the
-            # account that was denoted in the options.user passed to create(),
-            # by associating it with the credentialId and credentialPublicKey in
-            # the attestedCredentialData in authData, as appropriate for the
-            # Relying Party's system.
-            credential = WebAuthnCredential(rp_id=self.rp_id,
-                                            origin=self.origin,
-                                            aaguid=aaguid,
-                                            credential_id=b64_cred_id,
-                                            public_key=webauthn_b64_encode(credential_public_key),
-                                            sign_count=struct.unpack('!I', auth_data[33:37])[0],
-                                            attestation_level=attestation_level,
-                                            attestation_cert=trust_path[0] if trust_path else None)
-            if is_trusted_attestation_cert:
-                return credential
-
-            # Step 19.
-            #
-            # If the attestation statement attStmt successfully verified but is
-            # not trustworthy per step 16 above, the Relying Party SHOULD fail
-            # the registration ceremony.
-            #
-            #     NOTE: However, if permitted by policy, the Relying Party MAY
-            #           register the credential ID and credential public key but
-            #           treat the credential as one with self attestation (see
-            #           6.3.3 Attestation Types). If doing so, the Relying Party
-            #           is asserting there is no cryptographic proof that the
-            #           public key credential has been generated by a particular
-            #           authenticator model. See [FIDOSecRef] and [UAFProtocol]
-            #           for a more detailed discussion.
-            if self.trusted_attestation_cert_required:
-                raise RegistrationRejectedException('Untrusted attestation certificate.')
-            if not is_signed_attestation_cert and not self.none_attestation_permitted:
-                raise RegistrationRejectedException('No (or unsupported) attestation certificate.')
-            return credential
-
-        except Exception as e:
-            raise RegistrationRejectedException('Registration rejected. Error: {}'.format(e))
-
 
 class WebAuthnAssertionResponse(object):
     """
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 995ae2ef8..df099ce4f 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -882,7 +882,6 @@ class WebAuthnTokenClass(TokenClass):
             ).verify([
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
-
             self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id)))
             self.set_otp_count(web_authn_credential.sign_count)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
@@ -904,38 +903,6 @@ class WebAuthnTokenClass(TokenClass):
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
                                web_authn_credential.credential_device_type)
 
-            '''
-            self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.public_key))))
-            self.add_tokeninfo(WEBAUTHNINFO.ORIGIN,
-                               web_authn_credential.origin)
-            self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_LEVEL,
-                               web_authn_credential.attestation_level)
-
-            self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
-                               hexlify_and_unicode(web_authn_credential.aaguid))
-
-            # Add attestation info.
-            if web_authn_credential.attestation_cert:
-                # attestation_cert is of type X509. If you get warnings from your IDE
-                # here, it is because your IDE mistakenly assumes it to be of type PKey,
-                # due to a bug in pyOpenSSL 18.0.0. This bug is – however – purely
-                # cosmetic (a wrongly hinted return type in X509.from_cryptography()),
-                # and can be safely ignored.
-                #
-                # See also:
-                # https://github.com/pyca/pyopenssl/commit/4121e2555d07bbba501ac237408a0eea1b41f467
-                attestation_cert = crypto.X509.from_cryptography(web_authn_credential.attestation_cert)
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER,
-                                   x509name_to_string(attestation_cert.get_issuer()))
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT,
-                                   x509name_to_string(attestation_cert.get_subject()))
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL,
-                                   attestation_cert.get_serial_number())
-
-                cn = web_authn_credential.attestation_cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
-                automatic_description = cn[0].value if len(cn) else None
-            '''
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
             if not self.token.description:
@@ -1216,7 +1183,8 @@ class WebAuthnTokenClass(TokenClass):
             client_data = getParam(options, "clientdata", required)
             signature_data = getParam(options, "signaturedata", required)
             user_handle = getParam(options, "userhandle", optional)
-            assertion_client_extensions = getParam(options, "assertionclientextensions", optional)
+            uv_required = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
+
             try:
                 user = self._get_webauthn_user(getParam(options, "user", required))
             except ParameterError:
@@ -1246,85 +1214,16 @@ class WebAuthnTokenClass(TokenClass):
                     expected_origin=options["HTTP_ORIGIN"],
                     credential_public_key=user.public_key,
                     credential_current_sign_count=user.sign_count,
-                    require_user_verification=options.get(
-                        WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, False)
+                    require_user_verification=uv_required
                 )
                 sign_count = verified_authentication.new_sign_count
                 self.set_otp_count(sign_count)
+
                 return sign_count
 
             except InvalidAuthenticationResponse as ex:
                 raise AuthenticationRejectedException from ex
 
-            uv_req = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
-
-            challenge = binascii.unhexlify(getParam(options, "challenge", required))
-
-            try:
-                try:
-                    http_origin = getParam(options, "HTTP_ORIGIN", required, allow_empty=False)
-                except ParameterError:
-                    raise AuthenticationRejectedException('HTTP Origin header missing.')
-
-                # This does the heavy lifting.
-                #
-                # All data is parsed and verified. If any errors occur, an exception
-                # will be raised.
-                self.set_otp_count(WebAuthnAssertionResponse(
-                                       webauthn_user=user,
-                                       assertion_response={
-                                           'id': credential_id,
-                                           'userHandle': user_handle,
-                                           'clientData': client_data,
-                                           'authData': authenticator_data,
-                                           'signature': signature_data,
-                                           'assertionClientExtensions':
-                                               webauthn_b64_decode(assertion_client_extensions)
-                                                   if assertion_client_extensions
-                                                   else None
-                                       },
-                                       challenge=webauthn_b64_encode(challenge),
-                                       origin=http_origin,
-                                       allow_credentials=[user.credential_id],
-                                       uv_required=uv_req
-                                   ).verify())
-            except AuthenticationRejectedException as e:
-                # The authentication ceremony failed.
-                log.warning("Checking response for token {0!s} failed. {1!s}".format(self.token.serial, e))
-                return -1
-
-            # At this point we can check, if the attestation certificate is
-            # authorized. If not, we can raise a policy exception.
-            if not attestation_certificate_allowed(
-                    {
-                        "attestation_issuer": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER),
-                        "attestation_serial": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL),
-                        "attestation_subject": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT)
-                    },
-                    getParam(options, WEBAUTHNACTION.REQ, optional)
-            ):
-                log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy restriction {1!s}"
-                        .format(self.token.serial, WEBAUTHNACTION.REQ))
-                raise PolicyError("The WebAuthn token is not allowed to authenticate due to a policy restriction.")
-
-            # Now we need to check, if a whitelist for AAGUIDs exists, and if
-            # so, if this device is whitelisted. If not, we again raise a
-            # policy exception.
-            allowed_aaguids = getParam(options, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST, optional)
-            if allowed_aaguids and self.get_tokeninfo(WEBAUTHNINFO.AAGUID) not in allowed_aaguids:
-                log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy restriction {1!s}"
-                        .format(self.token.serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-                raise PolicyError("The WebAuthn token is not allowed to authenticate due to a policy restriction.")
-
-            # All clear? Nice!
-            return self.get_otp_count()
-
-        else:
-            # Not all necessary data provided.
-            return -1
-
 
 def is_webauthn_assertion_response(request_data):
     """
-- 
GitLab


From 543dd055bd1d39a07ee5e53b8b0e9c6c8b07befe Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 11:41:27 +0200
Subject: [PATCH 17/26] fix: added_repeated_registration_check_on_credential

---
 privacyidea/lib/tokens/webauthn_new.py | 100 +++++++++++++------------
 1 file changed, 52 insertions(+), 48 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthn_new.py b/privacyidea/lib/tokens/webauthn_new.py
index 41b50bdcc..7c5e802dc 100644
--- a/privacyidea/lib/tokens/webauthn_new.py
+++ b/privacyidea/lib/tokens/webauthn_new.py
@@ -1195,67 +1195,71 @@ class WebAuthnRegistrationResponse(object):
             )
 
     def extract_credential_id(self):
-        # decoded_response = dict(
-        #     [(k, webauthn_b64_decode(v)) for k, v in self.registration_response.items()])
         attestation_object = self.registration_response["attObj"]
         decoded_attestation_object = cbor2.loads(webauthn_b64_decode(attestation_object))
         auth_data = decoded_attestation_object["authData"]
         attestation_data = auth_data[37:]
         credential_id_len = struct.unpack('!H', attestation_data[16:18])[0]
         credential_id = attestation_data[18:18 + credential_id_len]
+
         return credential_id
 
-    def verify(self, existing_credential_ids=None):
-        """
-        Verify the WebAuthnRegistrationResponse.
 
-        This will perform the registration ceremony for the
-        WebAuthnRegistrationResponse. It will only return on successful
-        verification. In any other case, an appropriate error will be raised.
+def verify(self, existing_credential_ids=None):
+    """
+    Verify the WebAuthnRegistrationResponse.
 
-        :param existing_credential_ids: A list of existing credential ids to check for duplicates.
-        :type existing_credential_ids: basestring[]
-        :return: The WebAuthnCredential produced by the registration ceremony.
-        :rtype: VerifiedRegistration
-        """
+    This will perform the registration ceremony for the
+    WebAuthnRegistrationResponse. It will only return on successful
+    verification. In any other case, an appropriate error will be raised.
 
-        try:
-            credential_raw_id = self.extract_credential_id()
-            credential_id = bytes_to_base64url(credential_raw_id)
-            response = {
-                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
-                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
-            }
+    :param existing_credential_ids: A list of existing credential ids to check for duplicates.
+    :type existing_credential_ids: basestring[]
+    :return: The WebAuthnCredential produced by the registration ceremony.
+    :rtype: VerifiedRegistration
+    """
 
-            raw_registration_response = {
-                # need to make id == base64url(rawID)
-                "rawId": credential_raw_id,
-                "id": credential_id,
-                "response": response
-            }
+    try:
+        credential_raw_id = self.extract_credential_id()
+        credential_id = bytes_to_base64url(credential_raw_id)
 
-            verified_registration = verify_registration_response(
-                credential=RegistrationCredential.parse_obj(raw_registration_response),
-                expected_challenge=base64url_to_bytes(self.challenge),
-                expected_rp_id=self.rp_id,
-                expected_origin=self.origin,
-                require_user_verification=self.uv_required
-            )
-            return verified_registration
-            credential = WebAuthnCredential(
-                rp_id=self.rp_id,
-                origin=self.origin,
-                aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
-                credential_id=verified_registration.credential_id,
-                public_key=verified_registration.credential_public_key,
-                sign_count=verified_registration.sign_count,
-                # extract from fmt or attestation_object?
-                attestation_level="none",
-                attestation_cert=None
-            )
-            return credential
-        except InvalidRegistrationResponse as ex:
-            raise RegistrationRejectedException from ex
+        if existing_credential_ids and credential_id in existing_credential_ids:
+            raise RegistrationRejectedException('Credential already exists.')
+
+        response = {
+            "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+            "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+        }
+
+        raw_registration_response = {
+            # need to make id == base64url(rawID)
+            "rawId": credential_raw_id,
+            "id": credential_id,
+            "response": response
+        }
+
+        verified_registration = verify_registration_response(
+            credential=RegistrationCredential.parse_obj(raw_registration_response),
+            expected_challenge=base64url_to_bytes(self.challenge),
+            expected_rp_id=self.rp_id,
+            expected_origin=self.origin,
+            require_user_verification=self.uv_required
+        )
+        return verified_registration
+        credential = WebAuthnCredential(
+            rp_id=self.rp_id,
+            origin=self.origin,
+            aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
+            credential_id=verified_registration.credential_id,
+            public_key=verified_registration.credential_public_key,
+            sign_count=verified_registration.sign_count,
+            # extract from fmt or attestation_object?
+            attestation_level="none",
+            attestation_cert=None
+        )
+        return credential
+    except InvalidRegistrationResponse as ex:
+        raise RegistrationRejectedException from ex
 
 
 class WebAuthnAssertionResponse(object):
-- 
GitLab


From d1a23ef0cd3d6760355dcd57f508b4ca222d8fdd Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 24 Oct 2022 10:21:30 +0200
Subject: [PATCH 18/26] feat: rebased_to_newer_version

---
 .../tokens/{webauthn_new.py => webauthn.py}   |  99 +++--
 privacyidea/lib/tokens/webauthntoken.py       |  59 ++-
 tests/test_lib_tokens_webauthn.py             | 359 +++---------------
 3 files changed, 139 insertions(+), 378 deletions(-)
 rename privacyidea/lib/tokens/{webauthn_new.py => webauthn.py} (96%)

diff --git a/privacyidea/lib/tokens/webauthn_new.py b/privacyidea/lib/tokens/webauthn.py
similarity index 96%
rename from privacyidea/lib/tokens/webauthn_new.py
rename to privacyidea/lib/tokens/webauthn.py
index 7c5e802dc..0f79c87df 100644
--- a/privacyidea/lib/tokens/webauthn_new.py
+++ b/privacyidea/lib/tokens/webauthn.py
@@ -66,8 +66,10 @@ from cryptography import x509
 from cryptography.exceptions import InvalidSignature
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import constant_time
-from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1, ECDSA
-from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, MGF1
+from cryptography.hazmat.primitives.asymmetric.ec import \
+    EllipticCurvePublicNumbers, SECP256R1, ECDSA
+from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, \
+    MGF1
 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
 from cryptography.hazmat.primitives.hashes import SHA256
 from cryptography.x509 import load_der_x509_certificate
@@ -75,8 +77,9 @@ from webauthn import verify_registration_response, base64url_to_bytes
 from webauthn.helpers import bytes_to_base64url
 from webauthn.helpers.exceptions import InvalidRegistrationResponse
 from webauthn.helpers.structs import RegistrationCredential
+from webauthn.registration.verify_registration_response import \
+    VerifiedRegistration
 
-from privacyidea.lib import challenge
 from privacyidea.lib.tokens.u2f import url_encode, url_decode
 from privacyidea.lib.utils import to_bytes, to_unicode
 
@@ -1205,61 +1208,53 @@ class WebAuthnRegistrationResponse(object):
         return credential_id
 
 
-def verify(self, existing_credential_ids=None):
-    """
-    Verify the WebAuthnRegistrationResponse.
-
-    This will perform the registration ceremony for the
-    WebAuthnRegistrationResponse. It will only return on successful
-    verification. In any other case, an appropriate error will be raised.
+    def verify(self, existing_credential_ids=None):
+        """
+        Verify the WebAuthnRegistrationResponse.
+
+        This will perform the registration ceremony for the
+        WebAuthnRegistrationResponse. It will only return on successful
+        verification. In any other case, an appropriate error will
+        be raised.
+
+        :param existing_credential_ids: A list of existing
+        credential ids to check for duplicates.
+        :type existing_credential_ids: basestring[]
+        :return: The VerifiedRegistration produced by the registration
+        ceremony.
+        :rtype: VerifiedRegistration
+        """
 
-    :param existing_credential_ids: A list of existing credential ids to check for duplicates.
-    :type existing_credential_ids: basestring[]
-    :return: The WebAuthnCredential produced by the registration ceremony.
-    :rtype: VerifiedRegistration
-    """
+        try:
+            credential_raw_id = self.extract_credential_id()
+            credential_id = bytes_to_base64url(credential_raw_id)
 
-    try:
-        credential_raw_id = self.extract_credential_id()
-        credential_id = bytes_to_base64url(credential_raw_id)
+            if existing_credential_ids and credential_id in existing_credential_ids:
+                raise RegistrationRejectedException('Credential already exists.')
 
-        if existing_credential_ids and credential_id in existing_credential_ids:
-            raise RegistrationRejectedException('Credential already exists.')
+            response = {
+                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+            }
 
-        response = {
-            "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
-            "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
-        }
+            raw_registration_response = {
+                # need to make id == base64url(rawID)
+                "rawId": credential_raw_id,
+                "id": credential_id,
+                "response": response
+            }
 
-        raw_registration_response = {
-            # need to make id == base64url(rawID)
-            "rawId": credential_raw_id,
-            "id": credential_id,
-            "response": response
-        }
+            verified_registration = verify_registration_response(
+                credential=RegistrationCredential.parse_obj(raw_registration_response),
+                expected_challenge=base64url_to_bytes(self.challenge),
+                expected_rp_id=self.rp_id,
+                expected_origin=self.origin,
+                require_user_verification=self.uv_required
+            )
+            return verified_registration
 
-        verified_registration = verify_registration_response(
-            credential=RegistrationCredential.parse_obj(raw_registration_response),
-            expected_challenge=base64url_to_bytes(self.challenge),
-            expected_rp_id=self.rp_id,
-            expected_origin=self.origin,
-            require_user_verification=self.uv_required
-        )
-        return verified_registration
-        credential = WebAuthnCredential(
-            rp_id=self.rp_id,
-            origin=self.origin,
-            aaguid=bytes(verified_registration.aaguid, encoding="utf-8"),
-            credential_id=verified_registration.credential_id,
-            public_key=verified_registration.credential_public_key,
-            sign_count=verified_registration.sign_count,
-            # extract from fmt or attestation_object?
-            attestation_level="none",
-            attestation_cert=None
-        )
-        return credential
-    except InvalidRegistrationResponse as ex:
-        raise RegistrationRejectedException from ex
+        except InvalidRegistrationResponse as ex:
+            raise RegistrationRejectedException from ex
 
 
 class WebAuthnAssertionResponse(object):
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index df099ce4f..16451e83d 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -24,38 +24,33 @@
 import binascii
 import logging
 
-from OpenSSL import crypto
-from cryptography import x509
 from webauthn import verify_authentication_response, base64url_to_bytes
 from webauthn.helpers import bytes_to_base64url
 from webauthn.helpers.exceptions import InvalidAuthenticationResponse
 from webauthn.helpers.structs import AuthenticationCredential
 
-from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
+from privacyidea.api.lib.utils import getParam
 from privacyidea.lib import _
 from privacyidea.lib.challenge import get_challenges
 from privacyidea.lib.config import get_from_config
 from privacyidea.lib.crypto import geturandom
 from privacyidea.lib.decorators import check_token_locked
-from privacyidea.lib.error import ParameterError, RegistrationError, \
-    PolicyError
+from privacyidea.lib.error import ParameterError, RegistrationError
 from privacyidea.lib.log import log_with
 from privacyidea.lib.policy import SCOPE, GROUP, ACTION
 from privacyidea.lib.token import get_tokens
 from privacyidea.lib.tokenclass import TokenClass, CLIENTMODE, ROLLOUTSTATE
-from privacyidea.lib.tokens.u2f import x509name_to_string
 from privacyidea.lib.tokens.u2ftoken import IMAGES
-from privacyidea.lib.tokens.webauthn_new import (COSE_ALGORITHM,
-                                                 webauthn_b64_encode,
-                                                 WebAuthnRegistrationResponse,
-                                                 ATTESTATION_REQUIREMENT_LEVEL,
-                                                 webauthn_b64_decode,
-                                                 WebAuthnMakeCredentialOptions,
-                                                 WebAuthnAssertionOptions,
-                                                 WebAuthnUser,
-                                                 WebAuthnAssertionResponse,
-                                                 AuthenticationRejectedException,
-                                                 USER_VERIFICATION_LEVEL)
+from privacyidea.lib.tokens.webauthn import (COSE_ALGORITHM,
+                                             webauthn_b64_encode,
+                                             WebAuthnRegistrationResponse,
+                                             ATTESTATION_REQUIREMENT_LEVEL,
+                                             webauthn_b64_decode,
+                                             WebAuthnMakeCredentialOptions,
+                                             WebAuthnAssertionOptions,
+                                             WebAuthnUser,
+                                             AuthenticationRejectedException,
+                                             USER_VERIFICATION_LEVEL)
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import hexlify_and_unicode, is_true
 
@@ -864,7 +859,7 @@ class WebAuthnTokenClass(TokenClass):
             #
             # All data is parsed and verified. If any errors occur an exception
             # will be raised.
-            web_authn_credential = WebAuthnRegistrationResponse(
+            verified_registration = WebAuthnRegistrationResponse(
                 rp_id=rp_id,
                 origin=http_origin,
                 registration_response={
@@ -882,26 +877,29 @@ class WebAuthnTokenClass(TokenClass):
             ).verify([
                 token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
             ])
-            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(web_authn_credential.credential_id)))
-            self.set_otp_count(web_authn_credential.sign_count)
+
+            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(verified_registration.credential_id)))
+            self.set_otp_count(verified_registration.sign_count)
+            self.add_tokeninfo(WEBAUTHNINFO.ORIGIN, http_origin)
+            self.add_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID, rp_id)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
                                    bytes_to_base64url(
-                                       web_authn_credential.credential_public_key))
+                                       verified_registration.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
                                hexlify_and_unicode(
-                                   web_authn_credential.aaguid))
+                                   verified_registration.aaguid))
             self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
-                               web_authn_credential.fmt)
+                               verified_registration.fmt)
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
-                               web_authn_credential.credential_type)
+                               verified_registration.credential_type)
             self.add_tokeninfo(WEBAUTHNINFO.USER_VERIFIED,
-                               web_authn_credential.user_verified)
+                               verified_registration.user_verified)
             self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_OBJECT,
-                               bytes_to_base64url(web_authn_credential.attestation_object))
+                               bytes_to_base64url(verified_registration.attestation_object))
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_DEVICE_TYPE,
-                               web_authn_credential.credential_device_type)
+                               verified_registration.credential_device_type)
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
-                               web_authn_credential.credential_device_type)
+                               verified_registration.credential_device_type)
 
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
@@ -1212,7 +1210,7 @@ class WebAuthnTokenClass(TokenClass):
                     expected_challenge=binascii.unhexlify(options["challenge"]),
                     expected_rp_id=user.rp_id,
                     expected_origin=options["HTTP_ORIGIN"],
-                    credential_public_key=user.public_key,
+                    credential_public_key=base64url_to_bytes(user.public_key),
                     credential_current_sign_count=user.sign_count,
                     require_user_verification=uv_required
                 )
@@ -1222,7 +1220,8 @@ class WebAuthnTokenClass(TokenClass):
                 return sign_count
 
             except InvalidAuthenticationResponse as ex:
-                raise AuthenticationRejectedException from ex
+                log.warning(ex)
+                return -1
 
 
 def is_webauthn_assertion_response(request_data):
diff --git a/tests/test_lib_tokens_webauthn.py b/tests/test_lib_tokens_webauthn.py
index a795f87df..29b40ff69 100644
--- a/tests/test_lib_tokens_webauthn.py
+++ b/tests/test_lib_tokens_webauthn.py
@@ -62,7 +62,6 @@ import struct
 import unittest
 from copy import copy
 from mock import patch
-from testfixtures import log_capture
 from privacyidea.lib.user import User
 
 from privacyidea.lib.config import set_privacyidea_config
@@ -80,7 +79,6 @@ from privacyidea.lib.tokens.webauthntoken import (WebAuthnTokenClass, WEBAUTHNAC
 from privacyidea.lib.token import init_token, check_user_pass, remove_token
 from privacyidea.lib.policy import set_policy, SCOPE, ACTION, delete_policy
 from privacyidea.lib.challenge import get_challenges
-from privacyidea.lib.error import PolicyError, ParameterError, EnrollmentError
 
 TRUST_ANCHOR_DIR = "{}/testdata/trusted_attestation_roots".format(os.path.abspath(os.path.dirname(__file__)))
 REGISTRATION_RESPONSE_TMPL = {
@@ -116,12 +114,11 @@ CRED_KEY = {
     'alg': -7,
     'type': 'public-key'
 }
-CREDENTIAL_ID = b'ilNaaY5fYJoR1sg5IB7FL2Zoa-qBd_5Q95ZcyxNkmjkoDhiLCLgEKoKfCUElLt6_6Dmj_EUuOHZUI6x_gC32LQ'
 REGISTRATION_CHALLENGE = 'bPzpX3hHQtsp9evyKYkaZtVc9UN07PUdJ22vZUdDp94'
 ASSERTION_CHALLENGE = 'e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw'
 RP_ID = "webauthn.io"
 ORIGIN = "https://webauthn.io"
-USER_NAME = 'hans'
+USER_NAME = 'testuser'
 ICON_URL = "https://example.com/icon.png"
 USER_DISPLAY_NAME = "A Test User"
 USER_ID = b'\x80\xf1\xdc\xec\xb5\x18\xb1\xc8b\x05\x886\xbc\xdfJ\xdf'
@@ -171,47 +168,11 @@ NONE_ATTESTATION_CRED_ID = 'XyBvPatPc3biviiMX2Rkq5Vj-W6Pk6RtNi6r7v0dSgrLYaPxxWi0
 NONE_ATTESTATION_PUB_KEY = 'a5010203262001215820d210be8ddfb5b0bc0be1ea8deaedec197e43e1fece4eb95791e97e1d9219491f22582'\
                            '0f80e408b103d424808474999556a4e2c76453cfd114295a39bc9325a83b84416'
 
-SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL = {
-    "clientData": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjaGFsbGVuZ2Ui"
-                  "OiJBWGtYV1hQUDNnTHg4T0xscGtKM2FSUmhGV250blNFTmdnbmpEcEJxbDFu"
-                  "Z0tvbDd4V3dldlVZdnJwQkRQM0xFdmRyMkVPU3RPRnBHR3huTXZYay1WdyIs"
-                  "InR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
-    "attObj": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZzn__mNzaWdZAQCPypMLXWqtCZ1sc5Qd"
-              "jhH-pAzm8-adpfbemd5zsym2krscwV0EeOdTrdUOdy3hWj5HuK9dIX_OpNro2jKr"
-              "HfUj_0Kp-u87iqJ3MPzs-D9zXOqkbWqcY94Zh52wrPwhGfJ8BiQp5T4Q97E042hY"
-              "QRDKmtv7N-BT6dywiuFHxfm1sDbUZ_yyEIN3jgttJzjp_wvk_RJmb78bLPTlym83"
-              "Y0Ws73K6FFeiqFNqLA_8a4V0I088hs_IEPlj8PWxW0wnIUhI9IcRf0GEmUwTBpbN"
-              "DGpIFGOudnl_C3YuXuzK3R6pv2r7m9-9cIIeeYXD9BhSMBQ0A8oxBbVF7j-0xXDN"
-              "rXHZaGF1dGhEYXRhWQFnSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NB"
-              "AAAAOKjVmSRjt0nqud40p1PeHgEAIB-l9gZ544Ds7vzo_O76UZ8DCXiWFc8DN8LW"
-              "NZYQH0NepAEDAzn__iBZAQDAIqzybPPmgeL5OR6JKq9bWDiENJlN_LePQEnf1_sg"
-              "Om4FJ9kBTbOTtWplfoMXg40A7meMppiRqP72A3tmILwZ5xKIyY7V8Y2t8X1ilYJo"
-              "l2nCKOpAEqGLTRJjF64GQxen0uFpi1tA6l6N-ZboPxjky4aidBdUP22YZuEPCO8-"
-              "9ZTha8qwvTgZwMHhZ40TUPEJGGWOnHNlYmqnfFfk0P-UOZokI0rqtqqQGMwzV2Rr"
-              "H2kjKTZGfyskAQnrqf9PoJkye4KUjWkWnZzhkZbrDoLyTEX2oWvTTflnR5tAVMQc"
-              "h4UGgEHSZ00G5SFoc19nGx_UJcqezx5cLZsny-qQYDRjIUMBAAE"
-}
-
-SELF_ATTESTATION_REGISTRATION_RESPONSE_BAD_COSE_ALG = {
-    'clientData': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['clientData'],
-    'attObj': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][:529]
-              + 'y' + SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][530:]
-}
-
-SELF_ATTESTATION_REGISTRATION_RESPONSE_ALG_MISMATCH = {
-    'clientData': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['clientData'],
-    'attObj': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][:37]
-              + '2' + SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][38:]
-}
-
-SELF_ATTESTATION_REGISTRATION_RESPONSE_BROKEN_SIG = {
-    'clientData': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['clientData'],
-    'attObj': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][:46]
-              + 'AA' + SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][48:]
-}
-
 
 class WebAuthnTokenTestCase(MyTestCase):
+    USER_LOGIN = "testuser"
+    USER_REALM = "testrealm"
+    USER_RESOLVER = "testresolver"
 
     def _create_challenge(self):
         self.token.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(CRED_ID)))
@@ -220,37 +181,11 @@ class WebAuthnTokenTestCase(MyTestCase):
         (_, _, _, response_details) = self.token.create_challenge(options=self.challenge_options)
         return response_details
 
-    def _setup_token(self):
-        with patch('privacyidea.lib.tokens.webauthntoken.WebAuthnTokenClass._get_nonce') as mock_nonce:
-            mock_nonce.return_value = webauthn_b64_decode(REGISTRATION_CHALLENGE)
-            self.token.get_init_detail(self.init_params, self.user)
-        self.token.update({
-            'type': 'webauthn',
-            'serial': self.token.token.serial,
-            'regdata': REGISTRATION_RESPONSE_TMPL['attObj'],
-            'clientdata': REGISTRATION_RESPONSE_TMPL['clientData'],
-            WEBAUTHNACTION.RELYING_PARTY_ID: RP_ID,
-            WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_LEVEL: ATTESTATION_LEVEL.NONE,
-            'HTTP_ORIGIN': ORIGIN
-        })
-
     def setUp(self):
-        self.setUp_user_realms()
-
-        set_policy(name="WebAuthn",
-                   scope=SCOPE.ENROLL,
-                   action=WEBAUTHNACTION.RELYING_PARTY_NAME + "=" + RP_NAME + ","
-                          + WEBAUTHNACTION.RELYING_PARTY_ID + "=" + RP_ID)
-        set_privacyidea_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR, TRUST_ANCHOR_DIR)
-        set_privacyidea_config(WEBAUTHNCONFIG.APP_ID, APP_ID)
-
-        self.user = User(login=USER_NAME, realm=self.realm1,
-                         resolver=self.resolvername1)
-
         self.token = init_token({
             'type': 'webauthn',
             'pin': '1234'
-        }, user=self.user)
+        })
 
         self.init_params = {
             WEBAUTHNACTION.RELYING_PARTY_ID: RP_ID,
@@ -258,9 +193,11 @@ class WebAuthnTokenTestCase(MyTestCase):
             WEBAUTHNACTION.TIMEOUT: TIMEOUT,
             WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: DEFAULT_AUTHENTICATOR_ATTESTATION_FORM,
             WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT: DEFAULT_USER_VERIFICATION_REQUIREMENT,
-            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
+            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
         }
 
+        self.user = User(login=USER_NAME)
+
         self.challenge_options = {
             "user": self.user,
             WEBAUTHNACTION.ALLOWED_TRANSPORTS: ALLOWED_TRANSPORTS,
@@ -268,9 +205,15 @@ class WebAuthnTokenTestCase(MyTestCase):
             WEBAUTHNACTION.TIMEOUT: TIMEOUT
         }
 
-    def tearDown(self):
-        self.token.delete_token()
-        delete_policy("WebAuthn")
+    def test_00_users(self):
+        self.setUp_user_realms()
+
+        set_policy(name="WebAuthn",
+                   scope=SCOPE.ENROLL,
+                   action=WEBAUTHNACTION.RELYING_PARTY_NAME + "=" + RP_NAME + ","
+                         +WEBAUTHNACTION.RELYING_PARTY_ID   + "=" + RP_ID)
+        set_privacyidea_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR, TRUST_ANCHOR_DIR)
+        set_privacyidea_config(WEBAUTHNCONFIG.APP_ID, APP_ID)
 
     def test_01_create_token(self):
         self.assertEqual(self.token.type, "webauthn")
@@ -280,8 +223,6 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.assertTrue(self.token.token.serial.startswith("WAN"))
         with self.assertRaises(ValueError):
             self.token.get_init_detail()
-        with self.assertRaises(ParameterError):
-            self.token.get_init_detail(self.init_params)
 
     def test_02_token_init(self):
         web_authn_register_request = self\
@@ -294,23 +235,28 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.assertIn('name', web_authn_register_request.get("relyingParty"))
         self.assertEqual(RP_ID, web_authn_register_request.get("relyingParty").get('id'))
         self.assertEqual(RP_NAME, web_authn_register_request.get("relyingParty").get('name'))
-        self.assertIn('alg', web_authn_register_request.get("pubKeyCredAlgorithms")[0],
-                      web_authn_register_request)
-        self.assertIn('type', web_authn_register_request.get("pubKeyCredAlgorithms")[0],
-                      web_authn_register_request)
-        self.assertEqual('public-key',
-                         web_authn_register_request.get("pubKeyCredAlgorithms")[0].get("type"),
-                         web_authn_register_request)
-        self.assertEqual(COSE_ALGORITHM.ES256,
-                         web_authn_register_request.get("pubKeyCredAlgorithms")[0].get("alg"),
-                         web_authn_register_request)
+        self.assertIn('alg', web_authn_register_request.get("preferredAlgorithm"))
+        self.assertIn('type', web_authn_register_request.get("preferredAlgorithm"))
+        self.assertEqual('public-key', web_authn_register_request.get("preferredAlgorithm").get("type"))
+        self.assertEqual(COSE_ALGORITHM.ES256, web_authn_register_request.get("preferredAlgorithm").get("alg"))
         self.assertEqual(USER_NAME, web_authn_register_request.get("name"))
 
         self.assertEqual(RP_ID, self.token.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID))
         self.assertEqual(RP_NAME, self.token.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_NAME))
 
     def test_03_token_update(self):
-        self._setup_token()
+        with patch('privacyidea.lib.tokens.webauthntoken.WebAuthnTokenClass._get_nonce') as mock_nonce:
+            mock_nonce.return_value = webauthn_b64_decode(REGISTRATION_CHALLENGE)
+            self.token.get_init_detail(self.init_params, self.user)
+        self.token.update({
+            'type': 'webauthn',
+            'serial': self.token.token.serial,
+            'regdata': REGISTRATION_RESPONSE_TMPL['attObj'],
+            'clientdata': REGISTRATION_RESPONSE_TMPL['clientData'],
+            WEBAUTHNACTION.RELYING_PARTY_ID: RP_ID,
+            WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_LEVEL: ATTESTATION_LEVEL.NONE,
+            'HTTP_ORIGIN': ORIGIN
+        })
         web_authn_registration_response = self.token.get_init_detail().get("webAuthnRegisterResponse")
 
         self.assertTrue(web_authn_registration_response
@@ -334,31 +280,24 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.assertEqual(WebAuthnTokenClass.get_class_info('type'), "webauthn")
         self.assertTrue(self.token.token.serial.startswith("WAN"))
 
-        # we need to create an additional token for this to take effect
-        temp_token = init_token({
-            'type': 'webauthn',
-            'pin': '1234'
-        }, user=self.user)
-
         # No avoid double registration
         init_params = self.init_params
-        web_authn_register_request = temp_token \
+        web_authn_register_request = self \
+            .token \
             .get_init_detail(init_params, self.user) \
             .get("webAuthnRegisterRequest")
-        self.assertEqual(temp_token.token.serial, web_authn_register_request.get("serialNumber"))
+        self.assertEqual(self.token.token.serial, web_authn_register_request.get("serialNumber"))
         self.assertNotIn("excludeCredentials", web_authn_register_request)
 
-        self._setup_token()
-
         # Set avoid double registration
         init_params[WEBAUTHNACTION.AVOID_DOUBLE_REGISTRATION] = True
-        web_authn_register_request = temp_token \
+        web_authn_register_request = self \
+            .token \
             .get_init_detail(init_params, self.user) \
             .get("webAuthnRegisterRequest")
-        self.assertEqual(temp_token.token.serial, web_authn_register_request.get("serialNumber"))
+        self.assertEqual(self.token.token.serial, web_authn_register_request.get("serialNumber"))
         # Now the excludeCredentials is contained
         self.assertIn("excludeCredentials", web_authn_register_request)
-        temp_token.delete_token()
 
     def test_04_authentication(self):
         reply_dict = self._create_challenge()
@@ -386,27 +325,20 @@ class WebAuthnTokenTestCase(MyTestCase):
                                           })
         self.assertTrue(sign_count > 0)
 
-    @log_capture()
-    def test_06_missing_origin(self, capture):
+    def test_06_missing_origin(self):
+        self.challenge_options['nonce'] = webauthn_b64_decode(ASSERTION_CHALLENGE)
         self._create_challenge()
-        sign_count = self.token.check_otp(
-            otpval=None,
-            options={
-                "credentialid": CRED_ID,
-                "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-                "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-                "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-                "user": self.user,
-                "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-                "HTTP_ORIGIN": '',
-            })
+        sign_count = self.token.check_otp(otpval=None,
+                                          options={
+                                              "credentialid": CRED_ID,
+                                              "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
+                                              "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
+                                              "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
+                                              "user": self.user,
+                                              "challenge": hexlify_and_unicode(self.challenge_options['nonce']),
+                                              "HTTP_ORIGIN": '',
+                                          })
         self.assertTrue(sign_count == -1)
-        capture.check_present(
-            ('privacyidea.lib.tokens.webauthntoken',
-             'WARNING',
-             'Checking response for token {0!s} failed. HTTP Origin header '
-             'missing.'.format(self.token.get_serial()))
-        )
 
     def test_07_none_attestation(self):
         with patch('privacyidea.lib.tokens.webauthntoken.WebAuthnTokenClass._get_nonce') as mock_nonce:
@@ -433,7 +365,7 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.user = User(login=NONE_ATTESTATION_USER_NAME)
         self.token.get_init_detail(self.init_params, self.user)
 
-        with self.assertRaises(EnrollmentError):
+        with self.assertRaises(RegistrationRejectedException):
             self.token.update({
                 'type': 'webauthn',
                 'serial': self.token.token.serial,
@@ -444,78 +376,10 @@ class WebAuthnTokenTestCase(MyTestCase):
                 'HTTP_ORIGIN': ORIGIN
             })
 
-    def test_09a_attestation_subject_allowed(self):
-        self._setup_token()
-        options = {
-            "credentialid": CRED_ID,
-            "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-            "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-            "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-            "user": self.user,
-            "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-            "HTTP_ORIGIN": ORIGIN,
-            WEBAUTHNACTION.REQ: ['subject/.*Yubico.*/']
-        }
-        res = self.token.check_otp(otpval=None, options=options)
-        self.assertGreaterEqual(res, 0)
-
-    def test_09b_attestation_subject_not_allowed(self):
-        self._setup_token()
-        options = {
-            "credentialid": CRED_ID,
-            "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-            "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-            "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-            "user": self.user,
-            "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-            "HTTP_ORIGIN": ORIGIN,
-            WEBAUTHNACTION.REQ: ['subject/.*Feitian.*/']
-        }
-        self.assertRaisesRegexp(PolicyError,
-                                "The WebAuthn token is not allowed to authenticate "
-                                "due to a policy restriction.",
-                                self.token.check_otp,
-                                **{'otpval': None,
-                                   'options': options})
-
-    def test_10a_aaguid_allowed(self):
-        self._setup_token()
-        # check token with a valid aaguid
-        res = self.token.check_otp(otpval=None, options={
-            "credentialid": CRED_ID,
-            "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-            "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-            "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-            "user": self.user,
-            "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-            "HTTP_ORIGIN": ORIGIN,
-            WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST: ['00000000000000000000000000000000']
-        })
-        self.assertGreaterEqual(res, 0)
-
-    def test_10b_aaguid_not_allowed(self):
-        self._setup_token()
-        # check token with an invalid aaguid
-        self.assertRaisesRegexp(PolicyError,
-                                "The WebAuthn token is not allowed to authenticate "
-                                "due to a policy restriction.",
-                                self.token.check_otp,
-                                **{'otpval': None,
-                                   'options': {
-                                       "credentialid": CRED_ID,
-                                       "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-                                       "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-                                       "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-                                       "user": self.user,
-                                       "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-                                       "HTTP_ORIGIN": ORIGIN,
-                                       WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST: ['ffff0000000000000000000000000000']
-                                   }})
-
 
 class WebAuthnTestCase(unittest.TestCase):
     @staticmethod
-    def getWebAuthnCredential():
+    def getVerifiedRegistration():
         return WebAuthnRegistrationResponse(
             rp_id=RP_ID,
             origin=ORIGIN,
@@ -529,16 +393,16 @@ class WebAuthnTestCase(unittest.TestCase):
 
     @staticmethod
     def getAssertionResponse():
-        credential = WebAuthnTestCase.getWebAuthnCredential()
+        verified_registration = WebAuthnTestCase.getVerifiedRegistration()
         webauthn_user = WebAuthnUser(
             user_id=USER_ID,
             user_name=USER_NAME,
             user_display_name=USER_DISPLAY_NAME,
             icon_url=ICON_URL,
-            credential_id=credential.credential_id.decode(),
-            public_key=credential.public_key,
-            sign_count=credential.sign_count,
-            rp_id=credential.rp_id
+            credential_id=verified_registration.credential_id.decode(),
+            public_key=verified_registration.credential_public_key,
+            sign_count=verified_registration.sign_count,
+            rp_id=RP_ID
         )
 
         webauthn_assertion_response = WebAuthnAssertionResponse(
@@ -574,30 +438,9 @@ class WebAuthnTestCase(unittest.TestCase):
         self.assertTrue(CRED_KEY in registration_dict['pubKeyCredParams'])
 
     def test_01_validate_registration(self):
-        webauthn_credential = self.getWebAuthnCredential()
-        self.assertEqual(RP_ID, webauthn_credential.rp_id, webauthn_credential)
-        self.assertEqual(ORIGIN, webauthn_credential.origin, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_signed_attestation, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_trusted_attestation, webauthn_credential)
-        self.assertEqual(str(webauthn_credential),
-                         '{0!r} ({1!s}, {2!s}, {3!s})'.format(CREDENTIAL_ID,
-                                                              RP_ID, ORIGIN, 0),
-                         webauthn_credential)
-
-    def test_01b_validate_untrusted_registration(self):
-        webauthn_credential = WebAuthnRegistrationResponse(
-            rp_id=RP_ID,
-            origin=ORIGIN,
-            registration_response=copy(REGISTRATION_RESPONSE_TMPL),
-            challenge=REGISTRATION_CHALLENGE,
-            attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.NONE],
-            uv_required=False,
-            expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS,
-        ).verify()
-        self.assertEqual(RP_ID, webauthn_credential.rp_id, webauthn_credential)
-        self.assertEqual(ORIGIN, webauthn_credential.origin, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_signed_attestation, webauthn_credential)
-        self.assertFalse(webauthn_credential.has_trusted_attestation, webauthn_credential)
+        verified_registration = self.getVerifiedRegistration()
+        self.assertEqual(CRED_ID, verified_registration.credential_id)
+        self.assertEqual(CRED_KEY, verified_registration.credential_public_key)
 
     def test_02_registration_invalid_user_verification(self):
         registration_response = WebAuthnRegistrationResponse(
@@ -611,17 +454,11 @@ class WebAuthnTestCase(unittest.TestCase):
             expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
         )
 
-        with self.assertRaisesRegexp(RegistrationRejectedException,
-                                     'Malformed request received.'):
+        with self.assertRaises(RegistrationRejectedException):
             registration_response.verify()
 
     def test_03_validate_assertion(self):
         webauthn_assertion_response = self.getAssertionResponse()
-        webauthn_user = webauthn_assertion_response.webauthn_user
-        self.assertEqual(str(webauthn_user),
-                         '{0!r} ({1!s}, {2!s}, {3!s})'.format(USER_ID, USER_NAME,
-                                                              USER_DISPLAY_NAME, 0),
-                         webauthn_user)
         webauthn_assertion_response.verify()
 
     def test_04_invalid_signature_fail_assertion(self):
@@ -666,76 +503,6 @@ class WebAuthnTestCase(unittest.TestCase):
     def test_07_webauthn_b64_decode(self):
         self.assertEqual(webauthn_b64_decode(URL_DECODE_TEST_STRING), URL_DECODE_EXPECTED_RESULT)
 
-    def test_08_registration_invalid_requirement_level(self):
-        with self.assertRaisesRegexp(ValueError,
-                                     'Illegal attestation_requirement_level.'):
-            WebAuthnRegistrationResponse(
-                rp_id=RP_ID,
-                origin=ORIGIN,
-                registration_response=copy(REGISTRATION_RESPONSE_TMPL),
-                challenge=REGISTRATION_CHALLENGE,
-                attestation_requirement_level={'unknown level': False},
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                uv_required=True,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            )
-
-    def test_09_registration_self_Attestation(self):
-        webauthn_credential = WebAuthnRegistrationResponse(
-            rp_id='localhost',
-            origin='http://localhost:3000',
-            registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL),
-            challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-            attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-            trust_anchor_dir=TRUST_ANCHOR_DIR,
-            expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-        ).verify()
-        self.assertEqual('localhost', webauthn_credential.rp_id, webauthn_credential)
-        self.assertEqual('http://localhost:3000', webauthn_credential.origin, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_signed_attestation, webauthn_credential)
-        self.assertFalse(webauthn_credential.has_trusted_attestation, webauthn_credential)
-
-    def test_09b_registration_self_Attestation_bad_cose_alg(self):
-        self.assertRaises(
-            RegistrationRejectedException,
-            WebAuthnRegistrationResponse(
-                rp_id='localhost',
-                origin='http://localhost:3000',
-                registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_BAD_COSE_ALG),
-                challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            ).verify)
-
-    def test_09c_registration_self_Attestation_alg_mismatch(self):
-        self.assertRaisesRegexp(
-            RegistrationRejectedException,
-            'does not match algorithm from attestation statement',
-            WebAuthnRegistrationResponse(
-                rp_id='localhost',
-                origin='http://localhost:3000',
-                registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_ALG_MISMATCH),
-                challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            ).verify)
-
-    def test_09d_registration_self_Attestation_broken_signature(self):
-        self.assertRaisesRegexp(
-            RegistrationRejectedException,
-            'Invalid signature received.',
-            WebAuthnRegistrationResponse(
-                rp_id='localhost',
-                origin='http://localhost:3000',
-                registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_BROKEN_SIG),
-                challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            ).verify)
-
 
 class MultipleWebAuthnTokenTestCase(MyTestCase):
     rp_name = 'pitest'
@@ -806,7 +573,7 @@ class MultipleWebAuthnTokenTestCase(MyTestCase):
             WEBAUTHNACTION.TIMEOUT: TIMEOUT,
             WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: DEFAULT_AUTHENTICATOR_ATTESTATION_FORM,
             WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT: DEFAULT_USER_VERIFICATION_REQUIREMENT,
-            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
+            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
         }
     auth_options = {
         WEBAUTHNACTION.ALLOWED_TRANSPORTS: ALLOWED_TRANSPORTS,
-- 
GitLab


From 103b86bd18457c5e16f383d538bbc5203d23bf38 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 17 Oct 2022 14:03:06 +0200
Subject: [PATCH 19/26] fix: allowed_all_webauthntokens_in_api_prepolicy

---
 privacyidea/api/lib/prepolicy.py | 61 --------------------------------
 1 file changed, 61 deletions(-)

diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
index 8bf606147..40f71062a 100644
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -2026,67 +2026,6 @@ def webauthntoken_allowed(request, action):
     :return:
     :rtype:
     """
-
-    ttype = request.all_data.get("type")
-
-    # Get the registration data of the 2nd step of enrolling a WebAuthn token
-    reg_data = request.all_data.get("regdata")
-
-    # If a WebAuthn token is being enrolled.
-    if ttype and ttype.lower() == WebAuthnTokenClass.get_class_type() and reg_data:
-        serial = request.all_data.get("serial")
-        att_obj = WebAuthnRegistrationResponse.parse_attestation_object(reg_data)
-        (
-            attestation_type,
-            trust_path,
-            credential_pub_key,
-            cred_id,
-            aaguid
-        ) = WebAuthnRegistrationResponse.verify_attestation_statement(fmt=att_obj.get('fmt'),
-                                                                      att_stmt=att_obj.get('attStmt'),
-                                                                      auth_data=att_obj.get('authData'))
-
-        attestation_cert = crypto.X509.from_cryptography(trust_path[0]) if trust_path else None
-        allowed_certs_pols = Match\
-            .user(g,
-                  scope=SCOPE.ENROLL,
-                  action=WEBAUTHNACTION.REQ,
-                  user_object=request.User if hasattr(request, 'User') else None) \
-            .action_values(unique=False)
-
-        allowed_aaguids_pols = Match \
-            .user(g,
-                  scope=SCOPE.ENROLL,
-                  action=WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST,
-                  user_object=request.User if hasattr(request, 'User') else None) \
-            .action_values(unique=False,
-                           allow_white_space_in_action=True)
-        allowed_aaguids = set(
-            aaguid
-            for allowed_aaguid_pol in allowed_aaguids_pols
-            for aaguid in allowed_aaguid_pol.split()
-        )
-
-        # attestation_cert is of type X509. If you get a warning from your IDE
-        # here, it is because your IDE mistakenly assumes it to be of type PKey,
-        # due to a bug in pyOpenSSL 18.0.0. This bug is – however – purely
-        # cosmetic (a wrongly hinted return type in X509.from_cryptography()),
-        # and can be safely ignored.
-        #
-        # See also:
-        # https://github.com/pyca/pyopenssl/commit/4121e2555d07bbba501ac237408a0eea1b41f467
-        if allowed_certs_pols and not _attestation_certificate_allowed(attestation_cert, allowed_certs_pols):
-            log.warning(
-                "The WebAuthn token {0!s} is not allowed to be registered due to policy restriction {1!s}"
-                    .format(serial, WEBAUTHNACTION.REQ))
-            raise PolicyError("The WebAuthn token is not allowed to be registered due to a policy restriction.")
-
-        if allowed_aaguids and aaguid not in [allowed_aaguid.replace("-", "") for allowed_aaguid in allowed_aaguids]:
-            log.warning(
-                "The WebAuthn token {0!s} is not allowed to be registered due to policy restriction {1!s}"
-                    .format(serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-            raise PolicyError("The WebAuthn token is not allowed to be registered due to a policy restriction.")
-
     return True
 
 
-- 
GitLab


From f0faea893366b7dfd7abba9825bd787eed91eed0 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 19 Oct 2022 09:03:56 +0200
Subject: [PATCH 20/26] fix:
 removed_redundant_aaguid_modification_in_token_info

---
 privacyidea/lib/tokens/webauthntoken.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 16451e83d..765a6e7f9 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -886,8 +886,7 @@ class WebAuthnTokenClass(TokenClass):
                                    bytes_to_base64url(
                                        verified_registration.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
-                               hexlify_and_unicode(
-                                   verified_registration.aaguid))
+                               verified_registration.aaguid)
             self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
                                verified_registration.fmt)
             self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
-- 
GitLab


From 6e8436d8629bb2b89067fa973388a47f3ca0f4ce Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 24 Oct 2022 11:17:44 +0200
Subject: [PATCH 21/26] fix: added_missing_attrs_to_webauthntoken

---
 privacyidea/lib/tokens/webauthntoken.py | 49 +++++++++++--------------
 1 file changed, 22 insertions(+), 27 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 765a6e7f9..7cb883f65 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -424,28 +424,20 @@ DEFAULT_ALLOWED_TRANSPORTS = "usb ble nfc internal"
 DEFAULT_TIMEOUT = 60
 DEFAULT_USER_VERIFICATION_REQUIREMENT = 'preferred'
 DEFAULT_AUTHENTICATOR_ATTACHMENT = 'either'
-DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = 'ecdsa_preferred'
+DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = ['ecdsa', 'rsassa-pss']
 DEFAULT_AUTHENTICATOR_ATTESTATION_LEVEL = 'untrusted'
 DEFAULT_AUTHENTICATOR_ATTESTATION_FORM = 'direct'
 DEFAULT_CHALLENGE_TEXT_AUTH = _(u'Please confirm with your WebAuthn token ({0!s})')
 DEFAULT_CHALLENGE_TEXT_ENROLL = _(u'Please confirm with your WebAuthn token')
 
-PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE_OPTIONS = {
-    'ecdsa_preferred': [
-        COSE_ALGORITHM.ES256,
-        COSE_ALGORITHM.PS256
-    ],
-    'ecdsa_only': [
-        COSE_ALGORITHM.ES256
-    ],
-    'rsassa-pss_preferred': [
-        COSE_ALGORITHM.PS256,
-        COSE_ALGORITHM.ES256
-    ],
-    'rsassa-pss_only': [
-        COSE_ALGORITHM.PS256
-    ]
+PUBLIC_KEY_CREDENTIAL_ALGORITHMS = {
+    'ecdsa': COSE_ALGORITHM.ES256,
+    'rsassa-pss': COSE_ALGORITHM.PS256,
+    'rsassa-pkcs1v1_5': COSE_ALGORITHM.RS256
 }
+# since in Python < 3.7 the insert order of a dictionary is not guaranteed, we
+# need a list to define the proper order
+PUBKEY_CRED_ALGORITHMS_ORDER = ['ecdsa', 'rsassa-pss', 'rsassa-pkcs1v1_5']
 
 log = logging.getLogger(__name__)
 optional = True
@@ -480,7 +472,7 @@ class WEBAUTHNACTION(object):
     AUTHENTICATOR_ATTACHMENT = 'webauthn_authenticator_attachment'
     AUTHENTICATOR_SELECTION_LIST = 'webauthn_authenticator_selection_list'
     USER_VERIFICATION_REQUIREMENT = 'webauthn_user_verification_requirement'
-    PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = 'webauthn_public_key_credential_algorithm_preference'
+    PUBLIC_KEY_CREDENTIAL_ALGORITHMS = 'webauthn_public_key_credential_algorithms'
     AUTHENTICATOR_ATTESTATION_FORM = 'webauthn_authenticator_attestation_form'
     AUTHENTICATOR_ATTESTATION_LEVEL = 'webauthn_authenticator_attestation_level'
     REQ = 'webauthn_req'
@@ -669,17 +661,20 @@ class WebAuthnTokenClass(TokenClass):
                             "discouraged"
                         ]
                     },
-                    WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: {
+                    WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: {
                         'type': 'str',
-                        'desc': _("Which algorithm to use for creating public key credentials for WebAuthn tokens. "
-                                  "Default: ecdsa_preferred"),
+                        'desc': _(
+                            "Which algorithm are available to use for "
+                            "creating public key "
+                            "credentials for WebAuthn tokens. (Default: [{"
+                            "0!s}], Order: "
+                            "[{1!s}]".format(', '.join(
+                                DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE),
+                                             ', '.join(
+                                                 PUBKEY_CRED_ALGORITHMS_ORDER))),
                         'group': WEBAUTHNGROUP.WEBAUTHN,
-                        'value': [
-                            "ecdsa_preferred",
-                            "ecdsa_only",
-                            "rsassa-pss_preferred",
-                            "rsassa-pss_only"
-                        ]
+                        'multiple': True,
+                        'value': list(PUBLIC_KEY_CREDENTIAL_ALGORITHMS.keys())
                     },
                     WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: {
                         'type': 'str',
@@ -986,7 +981,7 @@ class WebAuthnTokenClass(TokenClass):
                                            WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT,
                                            required),
                 public_key_credential_algorithms=getParam(params,
-                                                          WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE,
+                                                          WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS,
                                                           required),
                 authenticator_attachment=getParam(params,
                                                   WEBAUTHNACTION.AUTHENTICATOR_ATTACHMENT,
-- 
GitLab


From 54d91f8bb7a4da57216dd2cedb76a049c8f37851 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 24 Oct 2022 13:03:08 +0200
Subject: [PATCH 22/26] fix: added_missing_attrs_to_webauthntoken

---
 privacyidea/lib/tokens/webauthntoken.py | 51 +++++++++++--------------
 1 file changed, 23 insertions(+), 28 deletions(-)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 765a6e7f9..40529b91f 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -424,28 +424,20 @@ DEFAULT_ALLOWED_TRANSPORTS = "usb ble nfc internal"
 DEFAULT_TIMEOUT = 60
 DEFAULT_USER_VERIFICATION_REQUIREMENT = 'preferred'
 DEFAULT_AUTHENTICATOR_ATTACHMENT = 'either'
-DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = 'ecdsa_preferred'
+DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = ['ecdsa', 'rsassa-pss']
 DEFAULT_AUTHENTICATOR_ATTESTATION_LEVEL = 'untrusted'
 DEFAULT_AUTHENTICATOR_ATTESTATION_FORM = 'direct'
 DEFAULT_CHALLENGE_TEXT_AUTH = _(u'Please confirm with your WebAuthn token ({0!s})')
 DEFAULT_CHALLENGE_TEXT_ENROLL = _(u'Please confirm with your WebAuthn token')
 
-PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE_OPTIONS = {
-    'ecdsa_preferred': [
-        COSE_ALGORITHM.ES256,
-        COSE_ALGORITHM.PS256
-    ],
-    'ecdsa_only': [
-        COSE_ALGORITHM.ES256
-    ],
-    'rsassa-pss_preferred': [
-        COSE_ALGORITHM.PS256,
-        COSE_ALGORITHM.ES256
-    ],
-    'rsassa-pss_only': [
-        COSE_ALGORITHM.PS256
-    ]
+PUBLIC_KEY_CREDENTIAL_ALGORITHMS = {
+    'ecdsa': COSE_ALGORITHM.ES256,
+    'rsassa-pss': COSE_ALGORITHM.PS256,
+    'rsassa-pkcs1v1_5': COSE_ALGORITHM.RS256
 }
+# since in Python < 3.7 the insert order of a dictionary is not guaranteed, we
+# need a list to define the proper order
+PUBKEY_CRED_ALGORITHMS_ORDER = ['ecdsa', 'rsassa-pss', 'rsassa-pkcs1v1_5']
 
 log = logging.getLogger(__name__)
 optional = True
@@ -480,7 +472,7 @@ class WEBAUTHNACTION(object):
     AUTHENTICATOR_ATTACHMENT = 'webauthn_authenticator_attachment'
     AUTHENTICATOR_SELECTION_LIST = 'webauthn_authenticator_selection_list'
     USER_VERIFICATION_REQUIREMENT = 'webauthn_user_verification_requirement'
-    PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE = 'webauthn_public_key_credential_algorithm_preference'
+    PUBLIC_KEY_CREDENTIAL_ALGORITHMS = 'webauthn_public_key_credential_algorithms'
     AUTHENTICATOR_ATTESTATION_FORM = 'webauthn_authenticator_attestation_form'
     AUTHENTICATOR_ATTESTATION_LEVEL = 'webauthn_authenticator_attestation_level'
     REQ = 'webauthn_req'
@@ -669,17 +661,20 @@ class WebAuthnTokenClass(TokenClass):
                             "discouraged"
                         ]
                     },
-                    WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: {
+                    WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: {
                         'type': 'str',
-                        'desc': _("Which algorithm to use for creating public key credentials for WebAuthn tokens. "
-                                  "Default: ecdsa_preferred"),
+                        'desc': _(
+                            "Which algorithm are available to use for "
+                            "creating public key "
+                            "credentials for WebAuthn tokens. (Default: [{"
+                            "0!s}], Order: "
+                            "[{1!s}]".format(', '.join(
+                                DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE),
+                                             ', '.join(
+                                                 PUBKEY_CRED_ALGORITHMS_ORDER))),
                         'group': WEBAUTHNGROUP.WEBAUTHN,
-                        'value': [
-                            "ecdsa_preferred",
-                            "ecdsa_only",
-                            "rsassa-pss_preferred",
-                            "rsassa-pss_only"
-                        ]
+                        'multiple': True,
+                        'value': list(PUBLIC_KEY_CREDENTIAL_ALGORITHMS.keys())
                     },
                     WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: {
                         'type': 'str',
@@ -986,7 +981,7 @@ class WebAuthnTokenClass(TokenClass):
                                            WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT,
                                            required),
                 public_key_credential_algorithms=getParam(params,
-                                                          WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE,
+                                                          WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS,
                                                           required),
                 authenticator_attachment=getParam(params,
                                                   WEBAUTHNACTION.AUTHENTICATOR_ATTACHMENT,
@@ -1003,7 +998,7 @@ class WebAuthnTokenClass(TokenClass):
                 "nonce": public_key_credential_creation_options["challenge"],
                 "relyingParty": public_key_credential_creation_options["rp"],
                 "serialNumber": public_key_credential_creation_options["user"]["id"],
-                "preferredAlgorithm": public_key_credential_creation_options["pubKeyCredParams"][0],
+                "pubKeyCredAlgorithms": public_key_credential_creation_options["pubKeyCredParams"],
                 "name": public_key_credential_creation_options["user"]["name"],
                 "displayName": public_key_credential_creation_options["user"]["displayName"]
             }
-- 
GitLab


From 30e5ac327a6447ac2c6f301d75752c537f901720 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Mon, 24 Oct 2022 13:03:33 +0200
Subject: [PATCH 23/26] feat: linked_missing_submodule

---
 .gitmodules | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitmodules b/.gitmodules
index 638f4e895..772abae58 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
 [submodule "privacyidea/static/contrib/js/webauthn-client"]
 	path = privacyidea/static/contrib/js/webauthn-client
-	url = https://github.com/privacyidea/webauthn-client
+	url = https://github.com/privacyidea/webauthn-client.git
-- 
GitLab


From 90a5e315de614e47ed21ea373fab437f1b4888e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavel=20B=C5=99ou=C5=A1ek?= <brousek@ics.muni.cz>
Date: Wed, 26 Oct 2022 22:54:03 +0200
Subject: [PATCH 24/26] fix(deps): add webauthn to requirements.txt

---
 requirements.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/requirements.txt b/requirements.txt
index 0eeb52bac..df3f0a48e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -106,3 +106,4 @@ zipp==3.8.0; python_version > '3.6'
 grpcio==1.46.0; python_version >= '3.7'
 grpcio-tools==1.46.0; python_version >= '3.7'
 protobuf==3.20.2; python_version >= '3.7'
+webauthn~=1.6
-- 
GitLab


From 0ab8817a90988c65981c4749aa0262b554b52a3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavel=20B=C5=99ou=C5=A1ek?= <brousek@ics.muni.cz>
Date: Wed, 26 Oct 2022 22:55:17 +0200
Subject: [PATCH 25/26] chore(deps): add webauthn dependency to setup.py

---
 setup.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setup.py b/setup.py
index 201949843..1a4d2b5dd 100644
--- a/setup.py
+++ b/setup.py
@@ -62,6 +62,7 @@ install_requires = ["beautifulsoup4[lxml]>=4.3.2",
                     "segno>=1.5",
                     "smpplib>=2.0",
                     "SQLAlchemy>=1.3.0,<1.4.0",
+                    "webauthn~=1.6",
                     "sqlsoup>=0.9.0"]
 
 
-- 
GitLab


From 8dc48d0d807ae238f8054aa28ea2eef85009e826 Mon Sep 17 00:00:00 2001
From: Peter Bolha <xbolha@fi.muni.cz>
Date: Wed, 9 Nov 2022 09:49:39 +0100
Subject: [PATCH 26/26] feat: add_support_for_migrated_webauthn_tokens

---
 privacyidea/lib/tokens/webauthntoken.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index 40529b91f..6f62b1994 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -1169,6 +1169,9 @@ class WebAuthnTokenClass(TokenClass):
         :return: A numerical value where values larger than zero indicate success.
         :rtype: int
         """
+        if self.get_tokeninfo("migrated") is not None:
+            return -1
+
         if is_webauthn_assertion_response(options) and getParam(options, "challenge", optional):
             credential_id = getParam(options, "credentialid", required)
             authenticator_data = getParam(options, "authenticatordata", required)
@@ -1216,6 +1219,8 @@ class WebAuthnTokenClass(TokenClass):
             except InvalidAuthenticationResponse as ex:
                 log.warning(ex)
                 return -1
+        else:
+            return -1
 
 
 def is_webauthn_assertion_response(request_data):
-- 
GitLab