Explorar o código

migrating from u2f-api.js to webauthn

FreddlePat %!s(int64=3) %!d(string=hai) anos
pai
achega
d1d134038f
Modificáronse 42 ficheiros con 918 adicións e 461 borrados
  1. 298 133
      data/web/inc/functions.inc.php
  2. 1 1
      data/web/inc/init_db.inc.php
  3. 42 25
      data/web/inc/lib/WebAuthn/Attestation/AttestationObject.php
  4. 4 4
      data/web/inc/lib/WebAuthn/Attestation/AuthenticatorData.php
  5. 5 4
      data/web/inc/lib/WebAuthn/Attestation/Format/AndroidKey.php
  6. 5 4
      data/web/inc/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php
  7. 5 4
      data/web/inc/lib/WebAuthn/Attestation/Format/Apple.php
  8. 7 6
      data/web/inc/lib/WebAuthn/Attestation/Format/FormatBase.php
  9. 7 5
      data/web/inc/lib/WebAuthn/Attestation/Format/None.php
  10. 5 4
      data/web/inc/lib/WebAuthn/Attestation/Format/Packed.php
  11. 5 4
      data/web/inc/lib/WebAuthn/Attestation/Format/Tpm.php
  12. 4 3
      data/web/inc/lib/WebAuthn/Attestation/Format/U2f.php
  13. 44 6
      data/web/inc/lib/WebAuthn/Binary/ByteBuffer.php
  14. 3 3
      data/web/inc/lib/WebAuthn/CBOR/CborDecoder.php
  15. 0 22
      data/web/inc/lib/WebAuthn/LICENSE
  16. 108 10
      data/web/inc/lib/WebAuthn/WebAuthn.php
  17. 1 1
      data/web/inc/lib/WebAuthn/WebAuthnException.php
  18. 16 12
      data/web/inc/prerequisites.inc.php
  19. 15 2
      data/web/inc/triggers.inc.php
  20. 6 0
      data/web/inc/vars.inc.php
  21. 90 17
      data/web/json_api.php
  22. 3 3
      data/web/lang/lang.ca.json
  23. 7 7
      data/web/lang/lang.cs.json
  24. 5 5
      data/web/lang/lang.da.json
  25. 5 5
      data/web/lang/lang.de.json
  26. 5 5
      data/web/lang/lang.en.json
  27. 5 5
      data/web/lang/lang.es.json
  28. 7 7
      data/web/lang/lang.fi.json
  29. 7 7
      data/web/lang/lang.fr.json
  30. 7 7
      data/web/lang/lang.it.json
  31. 7 7
      data/web/lang/lang.ko.json
  32. 3 3
      data/web/lang/lang.lv.json
  33. 7 7
      data/web/lang/lang.nl.json
  34. 3 3
      data/web/lang/lang.pl.json
  35. 7 7
      data/web/lang/lang.ro.json
  36. 5 5
      data/web/lang/lang.ru.json
  37. 5 5
      data/web/lang/lang.sk.json
  38. 6 6
      data/web/lang/lang.sv.json
  39. 7 7
      data/web/lang/lang.zh.json
  40. 1 1
      data/web/templates/admin/tab-config-admins.twig
  41. 98 62
      data/web/templates/base.twig
  42. 47 27
      data/web/templates/modals/footer.twig

+ 298 - 133
data/web/inc/functions.inc.php

@@ -1142,7 +1142,7 @@ function set_tfa($_data) {
   global $yubi;
   global $u2f;
   global $tfa;
-  $_data_log = $_data;
+  $_data_log = $_data["tfa_method"];
   !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
   $username = $_SESSION['mailcow_cc_username'];
   if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
@@ -1183,6 +1183,8 @@ function set_tfa($_data) {
       return false;
     }
   }
+
+
   switch ($_data["tfa_method"]) {
     case "yubi_otp":
       $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
@@ -1240,14 +1242,18 @@ function set_tfa($_data) {
         'msg' => array('object_modified', htmlspecialchars($username))
       );
     break;
+    // u2f - deprecated, should be removed
     case "u2f":
       $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
       try {
         $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_data['token']));
+
         $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");
         $stmt->execute(array(':username' => $username));
+
         $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");
         $stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
+
         $_SESSION['return'][] =  array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_data_log),
@@ -1286,6 +1292,29 @@ function set_tfa($_data) {
         );
       }
     break;
+    case "webauthn":
+        $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
+
+        $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'");
+        $stmt->execute(array(':username' => $username));
+
+        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
+        VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
+        $stmt->execute(array(
+            $username,
+            $key_id,
+            base64_encode($_data['registration']->credentialId),
+            $_data['registration']->credentialPublicKey,
+            $_data['registration']->certificate,
+            0
+        ));
+    
+        $_SESSION['return'][] =  array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_data_log),
+            'msg' => array('object_modified', $username)
+        );
+    break;
     case "none":
       $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
       $stmt->execute(array(':username' => $username));
@@ -1516,6 +1545,7 @@ function get_tfa($username = null) {
         }
         return $data;
       break;
+      // u2f - deprecated, should be removed
       case "u2f":
         $data['name'] = "u2f";
         $data['pretty'] = "Fido U2F";
@@ -1534,7 +1564,7 @@ function get_tfa($username = null) {
         $data['pretty'] = "HMAC-based OTP";
         return $data;
       break;
-       case "totp":
+      case "totp":
         $data['name'] = "totp";
         $data['pretty'] = "Time-based OTP";
         $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");
@@ -1546,7 +1576,20 @@ function get_tfa($username = null) {
           $data['additional'][] = $row;
         }
         return $data;
-        break;
+      break;
+      case "webauthn":
+        $data['name'] = "webauthn";
+        $data['pretty'] = "WebAuthn";
+        $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username");
+        $stmt->execute(array(
+          ':username' => $username,
+        ));
+        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+        while($row = array_shift($rows)) {
+          $data['additional'][] = $row;
+        }
+        return $data;
+      break;
       default:
         $data['name'] = 'none';
         $data['pretty'] = "-";
@@ -1560,140 +1603,261 @@ function get_tfa($username = null) {
     return $data;
   }
 }
-function verify_tfa_login($username, $token) {
-  global $pdo;
-  global $yubi;
-  global $u2f;
-  global $tfa;
-  $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
-      WHERE `username` = :username AND `active` = '1'");
-  $stmt->execute(array(':username' => $username));
-  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+function verify_tfa_login($username, $_data, $WebAuthn) {
+    global $pdo;
+    global $yubi;
+    global $u2f;
+    global $tfa;
+    $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
+        WHERE `username` = :username AND `active` = '1'");
+    $stmt->execute(array(':username' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
 
-  switch ($row["authmech"]) {
-    case "yubi_otp":
-      if (!ctype_alnum($token) || strlen($token) != 44) {
-        $_SESSION['return'][] =  array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $username, '*'),
-          'msg' => array('yotp_verification_failed', 'token length error')
-        );
-        return false;
-      }
-      $yubico_modhex_id = substr($token, 0, 12);
-      $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
-          WHERE `username` = :username
-          AND `authmech` = 'yubi_otp'
-          AND `active`='1'
-          AND `secret` LIKE :modhex");
-      $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
-      $row = $stmt->fetch(PDO::FETCH_ASSOC);
-      $yubico_auth = explode(':', $row['secret']);
-      $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
-      $yauth = $yubi->verify($token);
-      if (PEAR::isError($yauth)) {
-        $_SESSION['return'][] =  array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $username, '*'),
-          'msg' => array('yotp_verification_failed', $yauth->getMessage())
-        );
-        return false;
-      }
-      else {
-        $_SESSION['tfa_id'] = $row['id'];
-        $_SESSION['return'][] =  array(
-          'type' => 'success',
-          'log' => array(__FUNCTION__, $username, '*'),
-          'msg' => 'verified_yotp_login'
-        );
-        return true;
-      }
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $username, '*'),
-        'msg' => array('yotp_verification_failed', 'unknown')
-      );
-    return false;
-  break;
-  case "u2f":
-    try {
-      $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));
-      $stmt = $pdo->prepare("SELECT `id` FROM `tfa` WHERE `keyHandle` = ?");
-      $stmt->execute(array($reg->keyHandle));
-      $row_key_id = $stmt->fetch(PDO::FETCH_ASSOC);
-      $_SESSION['tfa_id'] = $row_key_id['id'];
-      $_SESSION['authReq'] = null;
-      $_SESSION['return'][] =  array(
-        'type' => 'success',
-        'log' => array(__FUNCTION__, $username, '*'),
-        'msg' => 'verified_u2f_login'
-      );
-      return true;
-    }
-    catch (Exception $e) {
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $username, '*'),
-        'msg' => array('u2f_verification_failed', $e->getMessage())
-      );
-      $_SESSION['regReq'] = null;
-      return false;
-    }
-    $_SESSION['return'][] =  array(
-      'type' => 'danger',
-      'log' => array(__FUNCTION__, $username, '*'),
-      'msg' => array('u2f_verification_failed', 'unknown')
-    );
-    return false;
-  break;
-  case "hotp":
-      return false;
-  break;
-  case "totp":
-    try {
-      $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
-          WHERE `username` = :username
-          AND `authmech` = 'totp'
-          AND `active`='1'");
-      $stmt->execute(array(':username' => $username));
-      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-      foreach ($rows as $row) {
-        if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) {
-          $_SESSION['tfa_id'] = $row['id'];
-          $_SESSION['return'][] =  array(
-            'type' => 'success',
+    switch ($row["authmech"]) {
+        case "yubi_otp":
+            if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('yotp_verification_failed', 'token length error')
+                );
+                return false;
+            }
+            $yubico_modhex_id = substr($_data['token'], 0, 12);
+            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
+                WHERE `username` = :username
+                AND `authmech` = 'yubi_otp'
+                AND `active`='1'
+                AND `secret` LIKE :modhex");
+            $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+            $row = $stmt->fetch(PDO::FETCH_ASSOC);
+            $yubico_auth = explode(':', $row['secret']);
+            $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
+            $yauth = $yubi->verify($_data['token']);
+            if (PEAR::isError($yauth)) {
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('yotp_verification_failed', $yauth->getMessage())
+                );
+                return false;
+            }
+            else {
+                $_SESSION['tfa_id'] = $row['id'];
+                $_SESSION['return'][] =  array(
+                    'type' => 'success',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => 'verified_yotp_login'
+                );
+                return true;
+            }
+            $_SESSION['return'][] =  array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $username, '*'),
+                'msg' => array('yotp_verification_failed', 'unknown')
+            );
+            return false;
+        break;
+        case "hotp":
+            return false;
+        break;
+        case "totp":
+            try {
+            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
+                WHERE `username` = :username
+                AND `authmech` = 'totp'
+                AND `active`='1'");
+            $stmt->execute(array(':username' => $username));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            foreach ($rows as $row) {
+                if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
+                $_SESSION['tfa_id'] = $row['id'];
+                $_SESSION['return'][] =  array(
+                    'type' => 'success',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => 'verified_totp_login'
+                );
+                return true;
+                }
+            }
+            $_SESSION['return'][] =  array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $username, '*'),
+                'msg' => 'totp_verification_failed'
+            );
+            return false;
+            }
+            catch (PDOException $e) {
+            $_SESSION['return'][] =  array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $username, '*'),
+                'msg' => array('mysql_error', $e)
+            );
+            return false;
+            }
+        break;
+        // u2f - deprecated, should be removed
+        case "u2f":
+            $tokenData = json_decode($_data['token']);
+            $clientDataJSON = base64_decode($tokenData->clientDataJSON);
+            $authenticatorData = base64_decode($tokenData->authenticatorData);
+            $signature = base64_decode($tokenData->signature);
+            $id = base64_decode($tokenData->id);
+            $challenge = $_SESSION['challenge'];
+
+            $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId");
+            $stmt->execute(array(':tokenId' => $tokenData->id));
+            $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
+
+            if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false;
+            
+            if ($process_webauthn['publicKey'] === false) {
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('webauthn_verification_failed', 'publicKey not found')
+                );
+                return false;
+            }
+            try {
+                $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
+            }
+            catch (Throwable $ex) {
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('webauthn_verification_failed', $ex->getMessage())
+                );
+                return false;
+            }
+
+            
+            $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
+            $stmt->execute(array(':username' => $process_webauthn['username']));
+            $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
+            if ($obj_props['superadmin'] === 1) {
+                $_SESSION["mailcow_cc_role"] = "admin";
+            }
+            elseif ($obj_props['superadmin'] === 0) {
+                $_SESSION["mailcow_cc_role"] = "domainadmin";
+            }
+            else {
+                $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
+                $stmt->execute(array(':username' => $process_webauthn['username']));
+                $row = $stmt->fetch(PDO::FETCH_ASSOC);
+                if ($row['username'] == $process_webauthn['username']) {
+                $_SESSION["mailcow_cc_role"] = "user";
+                }
+            }
+
+        
+            if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
+                );
+                return false;
+            }
+
+
+            $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
+            $_SESSION['tfa_id'] = $process_webauthn['key_id'];
+            $_SESSION['authReq'] = null;
+            unset($_SESSION["challenge"]);
+            $_SESSION['return'][] =  array(
+                'type' => 'success',
+                'log' => array("webauthn_login"),
+                'msg' => array('logged_in_as', $process_webauthn['username'])
+            );
+            return true;
+        break;
+        case "webauthn":
+            $tokenData = json_decode($_data['token']);
+            $clientDataJSON = base64_decode($tokenData->clientDataJSON);
+            $authenticatorData = base64_decode($tokenData->authenticatorData);
+            $signature = base64_decode($tokenData->signature);
+            $id = base64_decode($tokenData->id);
+            $challenge = $_SESSION['challenge'];
+
+            $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId");
+            $stmt->execute(array(':tokenId' => $tokenData->id));
+            $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
+
+            if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false;
+            
+            if ($process_webauthn['publicKey'] === false) {
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('webauthn_verification_failed', 'publicKey not found')
+                );
+                return false;
+            }
+            try {
+                $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
+            }
+            catch (Throwable $ex) {
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('webauthn_verification_failed', $ex->getMessage())
+                );
+                return false;
+            }
+
+            
+            $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
+            $stmt->execute(array(':username' => $process_webauthn['username']));
+            $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
+            if ($obj_props['superadmin'] === 1) {
+                $_SESSION["mailcow_cc_role"] = "admin";
+            }
+            elseif ($obj_props['superadmin'] === 0) {
+                $_SESSION["mailcow_cc_role"] = "domainadmin";
+            }
+            else {
+                $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
+                $stmt->execute(array(':username' => $process_webauthn['username']));
+                $row = $stmt->fetch(PDO::FETCH_ASSOC);
+                if ($row['username'] == $process_webauthn['username']) {
+                $_SESSION["mailcow_cc_role"] = "user";
+                }
+            }
+
+        
+            if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
+                $_SESSION['return'][] =  array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $username, '*'),
+                    'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
+                );
+                return false;
+            }
+
+
+            $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
+            $_SESSION['tfa_id'] = $process_webauthn['key_id'];
+            $_SESSION['authReq'] = null;
+            unset($_SESSION["challenge"]);
+            $_SESSION['return'][] =  array(
+                'type' => 'success',
+                'log' => array("webauthn_login"),
+                'msg' => array('logged_in_as', $process_webauthn['username'])
+            );
+            return true;
+        break;
+        default:
+            $_SESSION['return'][] =  array(
+            'type' => 'danger',
             'log' => array(__FUNCTION__, $username, '*'),
-            'msg' => 'verified_totp_login'
-          );
-          return true;
-        }
-      }
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $username, '*'),
-        'msg' => 'totp_verification_failed'
-      );
-      return false;
-    }
-    catch (PDOException $e) {
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $username, '*'),
-        'msg' => array('mysql_error', $e)
-      );
-      return false;
+            'msg' => 'unknown_tfa_method'
+            );
+            return false;
+        break;
     }
-  break;
-  default:
-    $_SESSION['return'][] =  array(
-      'type' => 'danger',
-      'log' => array(__FUNCTION__, $username, '*'),
-      'msg' => 'unknown_tfa_method'
-    );
+
     return false;
-  break;
-  }
-  return false;
 }
 function admin_api($access, $action, $data = null) {
   global $pdo;
@@ -1955,6 +2119,7 @@ function rspamd_ui($action, $data = null) {
     break;
   }
 }
+// u2f - deprecated, should be removed
 function get_u2f_registrations($username) {
   global $pdo;
   $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");

+ 1 - 1
data/web/inc/init_db.inc.php

@@ -696,7 +696,7 @@ function init_db_schema() {
           "id" => "INT NOT NULL AUTO_INCREMENT",
           "key_id" => "VARCHAR(255) NOT NULL",
           "username" => "VARCHAR(255) NOT NULL",
-          "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp')",
+          "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')",
           "secret" => "VARCHAR(255) DEFAULT NULL",
           "keyHandle" => "VARCHAR(255) DEFAULT NULL",
           "publicKey" => "VARCHAR(255) DEFAULT NULL",

+ 42 - 25
data/web/inc/lib/WebAuthn/Attestation/AttestationObject.php

@@ -1,9 +1,9 @@
 <?php
 
-namespace WebAuthn\Attestation;
-use WebAuthn\WebAuthnException;
-use WebAuthn\CBOR\CborDecoder;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\CBOR\CborDecoder;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 /**
  * @author Lukas Buchs
@@ -12,6 +12,7 @@ use WebAuthn\Binary\ByteBuffer;
 class AttestationObject {
     private $_authenticatorData;
     private $_attestationFormat;
+    private $_attestationFormatName;
 
     public function __construct($binary , $allowedFormats) {
         $enc = CborDecoder::decode($binary);
@@ -29,13 +30,15 @@ class AttestationObject {
         }
 
         $this->_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString());
+        $this->_attestationFormatName = $enc['fmt'];
 
         // Format ok?
-        if (!in_array($enc['fmt'], $allowedFormats)) {
-            throw new WebAuthnException('invalid atttestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
+        if (!in_array($this->_attestationFormatName, $allowedFormats)) {
+            throw new WebAuthnException('invalid atttestation format: ' . $this->_attestationFormatName, WebAuthnException::INVALID_DATA);
         }
 
-        switch ($enc['fmt']) {
+
+        switch ($this->_attestationFormatName) {
             case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break;
             case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break;
             case 'apple': $this->_attestationFormat = new Format\Apple($enc, $this->_authenticatorData); break;
@@ -47,6 +50,14 @@ class AttestationObject {
         }
     }
 
+    /**
+     * returns the attestation format name
+     * @return string
+     */
+    public function getAttestationFormatName() {
+        return $this->_attestationFormatName;
+    }
+
     /**
      * returns the attestation public key in PEM format
      * @return AuthenticatorData
@@ -72,16 +83,19 @@ class AttestationObject {
         $issuer = '';
         if ($pem) {
             $certInfo = \openssl_x509_parse($pem);
-            if (\is_array($certInfo) && \is_array($certInfo['issuer'])) {
-                if ($certInfo['issuer']['CN']) {
-                    $issuer .= \trim($certInfo['issuer']['CN']);
+            if (\is_array($certInfo) && \array_key_exists('issuer', $certInfo) && \is_array($certInfo['issuer'])) {
+
+                $cn = $certInfo['issuer']['CN'] ?? '';
+                $o = $certInfo['issuer']['O'] ?? '';
+                $ou = $certInfo['issuer']['OU'] ?? '';
+
+                if ($cn) {
+                    $issuer .= $cn;
                 }
-                if ($certInfo['issuer']['O'] || $certInfo['issuer']['OU']) {
-                    if ($issuer) {
-                        $issuer .= ' (' . \trim($certInfo['issuer']['O'] . ' ' . $certInfo['issuer']['OU']) . ')';
-                    } else {
-                        $issuer .= \trim($certInfo['issuer']['O'] . ' ' . $certInfo['issuer']['OU']);
-                    }
+                if ($issuer && ($o || $ou)) {
+                    $issuer .= ' (' . trim($o . ' ' . $ou) . ')';
+                } else {
+                    $issuer .= trim($o . ' ' . $ou);
                 }
             }
         }
@@ -98,16 +112,19 @@ class AttestationObject {
         $subject = '';
         if ($pem) {
             $certInfo = \openssl_x509_parse($pem);
-            if (\is_array($certInfo) && \is_array($certInfo['subject'])) {
-                if ($certInfo['subject']['CN']) {
-                    $subject .= \trim($certInfo['subject']['CN']);
+            if (\is_array($certInfo) && \array_key_exists('subject', $certInfo) && \is_array($certInfo['subject'])) {
+
+                $cn = $certInfo['subject']['CN'] ?? '';
+                $o = $certInfo['subject']['O'] ?? '';
+                $ou = $certInfo['subject']['OU'] ?? '';
+
+                if ($cn) {
+                    $subject .= $cn;
                 }
-                if ($certInfo['subject']['O'] || $certInfo['subject']['OU']) {
-                    if ($subject) {
-                        $subject .= ' (' . \trim($certInfo['subject']['O'] . ' ' . $certInfo['subject']['OU']) . ')';
-                    } else {
-                        $subject .= \trim($certInfo['subject']['O'] . ' ' . $certInfo['subject']['OU']);
-                    }
+                if ($subject && ($o || $ou)) {
+                    $subject .= ' (' . trim($o . ' ' . $ou) . ')';
+                } else {
+                    $subject .= trim($o . ' ' . $ou);
                 }
             }
         }

+ 4 - 4
data/web/inc/lib/WebAuthn/Attestation/AuthenticatorData.php

@@ -1,9 +1,9 @@
 <?php
 
-namespace WebAuthn\Attestation;
-use WebAuthn\WebAuthnException;
-use WebAuthn\CBOR\CborDecoder;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\CBOR\CborDecoder;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 /**
  * @author Lukas Buchs

+ 5 - 4
data/web/inc/lib/WebAuthn/Attestation/Format/AndroidKey.php

@@ -1,15 +1,16 @@
 <?php
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 class AndroidKey extends FormatBase {
     private $_alg;
     private $_signature;
     private $_x5c;
 
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         parent::__construct($AttestionObject, $authenticatorData);
 
         // check u2f data

+ 5 - 4
data/web/inc/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php

@@ -1,9 +1,10 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 class AndroidSafetyNet extends FormatBase {
     private $_signature;
@@ -11,7 +12,7 @@ class AndroidSafetyNet extends FormatBase {
     private $_x5c;
     private $_payload;
 
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         parent::__construct($AttestionObject, $authenticatorData);
 
         // check data

+ 5 - 4
data/web/inc/lib/WebAuthn/Attestation/Format/Apple.php

@@ -1,14 +1,15 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 class Apple extends FormatBase {
     private $_x5c;
 
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         parent::__construct($AttestionObject, $authenticatorData);
 
         // check packed data

+ 7 - 6
data/web/inc/lib/WebAuthn/Attestation/Format/FormatBase.php

@@ -1,8 +1,9 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
 
 
 abstract class FormatBase {
@@ -14,9 +15,9 @@ abstract class FormatBase {
     /**
      *
      * @param Array $AttestionObject
-     * @param \WebAuthn\Attestation\AuthenticatorData $authenticatorData
+     * @param AuthenticatorData $authenticatorData
      */
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         $this->_attestationObject = $AttestionObject;
         $this->_authenticatorData = $authenticatorData;
     }
@@ -26,7 +27,7 @@ abstract class FormatBase {
      */
     public function __destruct() {
         // delete X.509 chain certificate file after use
-        if (\is_file($this->_x5c_tempFile)) {
+        if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
             \unlink($this->_x5c_tempFile);
         }
     }
@@ -36,7 +37,7 @@ abstract class FormatBase {
      * @return string|null
      */
     public function getCertificateChain() {
-        if (\is_file($this->_x5c_tempFile)) {
+        if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
             return \file_get_contents($this->_x5c_tempFile);
         }
         return null;

+ 7 - 5
data/web/inc/lib/WebAuthn/Attestation/Format/None.php

@@ -1,13 +1,14 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
 
 class None extends FormatBase {
 
 
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         parent::__construct($AttestionObject, $authenticatorData);
     }
 
@@ -28,12 +29,13 @@ class None extends FormatBase {
     }
 
     /**
-     * validates the certificate against root certificates
+     * validates the certificate against root certificates.
+     * Format 'none' does not contain any ca, so always false.
      * @param array $rootCas
      * @return boolean
      * @throws WebAuthnException
      */
     public function validateRootCertificate($rootCas) {
-        return true;
+        return false;
     }
 }

+ 5 - 4
data/web/inc/lib/WebAuthn/Attestation/Format/Packed.php

@@ -1,16 +1,17 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 class Packed extends FormatBase {
     private $_alg;
     private $_signature;
     private $_x5c;
 
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         parent::__construct($AttestionObject, $authenticatorData);
 
         // check packed data

+ 5 - 4
data/web/inc/lib/WebAuthn/Attestation/Format/Tpm.php

@@ -1,9 +1,10 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 class Tpm extends FormatBase {
     private $_TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
@@ -19,7 +20,7 @@ class Tpm extends FormatBase {
     private $_certInfo;
 
 
-    public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
         parent::__construct($AttestionObject, $authenticatorData);
 
         // check packed data

+ 4 - 3
data/web/inc/lib/WebAuthn/Attestation/Format/U2f.php

@@ -1,9 +1,10 @@
 <?php
 
 
-namespace WebAuthn\Attestation\Format;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 class U2f extends FormatBase {
     private $_alg = -7;

+ 44 - 6
data/web/inc/lib/WebAuthn/Binary/ByteBuffer.php

@@ -1,8 +1,8 @@
 <?php
 
 
-namespace WebAuthn\Binary;
-use WebAuthn\WebAuthnException;
+namespace lbuchs\WebAuthn\Binary;
+use lbuchs\WebAuthn\WebAuthnException;
 
 /**
  * Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/ByteBuffer.php
@@ -39,7 +39,7 @@ class ByteBuffer implements \JsonSerializable, \Serializable {
     /**
      * create a ByteBuffer from a base64 url encoded string
      * @param string $base64url
-     * @return \WebAuthn\Binary\ByteBuffer
+     * @return ByteBuffer
      */
     public static function fromBase64Url($base64url) {
         $bin = self::_base64url_decode($base64url);
@@ -52,7 +52,7 @@ class ByteBuffer implements \JsonSerializable, \Serializable {
     /**
      * create a ByteBuffer from a base64 url encoded string
      * @param string $hex
-     * @return \WebAuthn\Binary\ByteBuffer
+     * @return ByteBuffer
      */
     public static function fromHex($hex) {
         $bin = \hex2bin($hex);
@@ -65,7 +65,7 @@ class ByteBuffer implements \JsonSerializable, \Serializable {
     /**
      * create a random ByteBuffer
      * @param string $length
-     * @return \WebAuthn\Binary\ByteBuffer
+     * @return ByteBuffer
      */
     public static function randomBuffer($length) {
         if (\function_exists('random_bytes')) { // >PHP 7.0
@@ -97,6 +97,14 @@ class ByteBuffer implements \JsonSerializable, \Serializable {
         return \ord(\substr($this->_data, $offset, 1));
     }
 
+    public function getJson($jsonFlags=0) {
+        $data = \json_decode($this->getBinaryString(), null, 512, $jsonFlags);
+        if (\json_last_error() !== JSON_ERROR_NONE) {
+            throw new WebAuthnException(\json_last_error_msg(), WebAuthnException::BYTEBUFFER);
+        }
+        return $data;
+    }
+
     public function getLength() {
         return $this->_length;
     }
@@ -203,7 +211,7 @@ class ByteBuffer implements \JsonSerializable, \Serializable {
     /**
      * jsonSerialize interface
      * return binary data in RFC 1342-Like serialized string
-     * @return \stdClass
+     * @return string
      */
     public function jsonSerialize() {
         if (ByteBuffer::$useBase64UrlEncoding) {
@@ -231,6 +239,36 @@ class ByteBuffer implements \JsonSerializable, \Serializable {
         $this->_length = \strlen($this->_data);
     }
 
+    /**
+     * (PHP 8 deprecates Serializable-Interface)
+     * @return array
+     */
+    public function __serialize() {
+        return [
+            'data' => \serialize($this->_data)
+        ];
+    }
+
+    /**
+     * object to string
+     * @return string
+     */
+    public function __toString() {
+        return $this->getHex();
+    }
+
+    /**
+     * (PHP 8 deprecates Serializable-Interface)
+     * @param array $data
+     * @return void
+     */
+    public function __unserialize($data) {
+        if ($data && isset($data['data'])) {
+            $this->_data = \unserialize($data['data']);
+            $this->_length = \strlen($this->_data);
+        }
+    }
+
     // -----------------------
     // PROTECTED STATIC
     // -----------------------

+ 3 - 3
data/web/inc/lib/WebAuthn/CBOR/CborDecoder.php

@@ -1,9 +1,9 @@
 <?php
 
 
-namespace WebAuthn\CBOR;
-use WebAuthn\WebAuthnException;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn\CBOR;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 
 /**
  * Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/CborDecoder.php

+ 0 - 22
data/web/inc/lib/WebAuthn/LICENSE

@@ -1,22 +0,0 @@
-MIT License
-
-Copyright © 2019 Lukas Buchs
-Copyright © 2018 Thomas Bleeker (CBOR & ByteBuffer part)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.

+ 108 - 10
data/web/inc/lib/WebAuthn/WebAuthn.php

@@ -1,7 +1,7 @@
 <?php
 
-namespace WebAuthn;
-use WebAuthn\Binary\ByteBuffer;
+namespace lbuchs\WebAuthn;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
 require_once 'WebAuthnException.php';
 require_once 'Binary/ByteBuffer.php';
 require_once 'Attestation/AttestationObject.php';
@@ -69,16 +69,20 @@ class WebAuthn {
     /**
      * add a root certificate to verify new registrations
      * @param string $path file path of / directory with root certificates
+     * @param array|null $certFileExtensions if adding a direction, all files with provided extension are added. default: pem, crt, cer, der
      */
-    public function addRootCertificates($path) {
+    public function addRootCertificates($path, $certFileExtensions=null) {
         if (!\is_array($this->_caFiles)) {
             $this->_caFiles = array();
         }
+        if ($certFileExtensions === null) {
+            $certFileExtensions = array('pem', 'crt', 'cer', 'der');
+        }
         $path = \rtrim(\trim($path), '\\/');
         if (\is_dir($path)) {
             foreach (\scandir($path) as $ca) {
-                if (\is_file($path . '/' . $ca)) {
-                    $this->addRootCertificates($path . '/' . $ca);
+                if (\is_file($path . DIRECTORY_SEPARATOR . $ca) && \in_array(\strtolower(\pathinfo($ca, PATHINFO_EXTENSION)), $certFileExtensions)) {
+                    $this->addRootCertificates($path . DIRECTORY_SEPARATOR . $ca);
                 }
             }
         } else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) {
@@ -273,10 +277,11 @@ class WebAuthn {
      * @param string|ByteBuffer $challenge binary used challange
      * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
      * @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button)
+     * @param bool $failIfRootMismatch false, if there should be no error thrown if root certificate doesn't match
      * @return \stdClass
      * @throws WebAuthnException
      */
-    public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true) {
+    public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) {
         $clientDataHash = \hash('sha256', $clientDataJSON, true);
         $clientData = \json_decode($clientDataJSON);
         $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
@@ -318,18 +323,21 @@ class WebAuthn {
         }
 
         // 15. If validation is successful, obtain a list of acceptable trust anchors
-        if (is_array($this->_caFiles) && !$attestationObject->validateRootCertificate($this->_caFiles)) {
+        $rootValid = is_array($this->_caFiles) ? $attestationObject->validateRootCertificate($this->_caFiles) : null;
+        if ($failIfRootMismatch && is_array($this->_caFiles) && !$rootValid) {
             throw new WebAuthnException('invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED);
         }
 
         // 10. Verify that the User Present bit of the flags in authData is set.
-        if ($requireUserPresent && !$attestationObject->getAuthenticatorData()->getUserPresent()) {
+        $userPresent = $attestationObject->getAuthenticatorData()->getUserPresent();
+        if ($requireUserPresent && !$userPresent) {
             throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
         }
 
         // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
-        if ($requireUserVerification && !$attestationObject->getAuthenticatorData()->getUserVerified()) {
-            throw new WebAuthnException('user not verificated during authentication', WebAuthnException::USER_VERIFICATED);
+        $userVerified = $attestationObject->getAuthenticatorData()->getUserVerified();
+        if ($requireUserVerification && !$userVerified) {
+            throw new WebAuthnException('user not verified during authentication', WebAuthnException::USER_VERIFICATED);
         }
 
         $signCount = $attestationObject->getAuthenticatorData()->getSignCount();
@@ -340,6 +348,7 @@ class WebAuthn {
         // prepare data to store for future logins
         $data = new \stdClass();
         $data->rpId = $this->_rpId;
+        $data->attestationFormat = $attestationObject->getAttestationFormatName();
         $data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId();
         $data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem();
         $data->certificateChain = $attestationObject->getCertificateChain();
@@ -348,6 +357,9 @@ class WebAuthn {
         $data->certificateSubject = $attestationObject->getCertificateSubject();
         $data->signatureCounter = $this->_signatureCounter;
         $data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID();
+        $data->rootValid = $rootValid;
+        $data->userPresent = $userPresent;
+        $data->userVerified = $userVerified;
         return $data;
     }
 
@@ -453,6 +465,92 @@ class WebAuthn {
         return true;
     }
 
+    /**
+     * Downloads root certificates from FIDO Alliance Metadata Service (MDS) to a specific folder
+     * https://fidoalliance.org/metadata/
+     * @param string $certFolder Folder path to save the certificates in PEM format.
+     * @param bool $deleteCerts=true
+     * @return int number of cetificates
+     * @throws WebAuthnException
+     */
+    public function queryFidoMetaDataService($certFolder, $deleteCerts=true) {
+        $url = 'https://mds.fidoalliance.org/';
+        $raw = null;
+        if (\function_exists('curl_init')) {
+            $ch = \curl_init($url);
+            \curl_setopt($ch, CURLOPT_HEADER, false);
+            \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+            \curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+            \curl_setopt($ch, CURLOPT_USERAGENT, 'github.com/lbuchs/WebAuthn - A simple PHP WebAuthn server library');
+            $raw = \curl_exec($ch);
+            \curl_close($ch);
+        } else {
+            $raw = \file_get_contents($url);
+        }
+
+        $certFolder = \rtrim(\realpath($certFolder), '\\/');
+        if (!is_dir($certFolder)) {
+            throw new WebAuthnException('Invalid folder path for query FIDO Alliance Metadata Service');
+        }
+
+        if (!\is_string($raw)) {
+            throw new WebAuthnException('Unable to query FIDO Alliance Metadata Service');
+        }
+
+        $jwt = \explode('.', $raw);
+        if (\count($jwt) !== 3) {
+            throw new WebAuthnException('Invalid JWT from FIDO Alliance Metadata Service');
+        }
+
+        if ($deleteCerts) {
+            foreach (\scandir($certFolder) as $ca) {
+                if (\substr($ca, -4) === '.pem') {
+                    if (\unlink($certFolder . DIRECTORY_SEPARATOR . $ca) === false) {
+                        throw new WebAuthnException('Cannot delete certs in folder for FIDO Alliance Metadata Service');
+                    }
+                }
+            }
+        }
+
+        list($header, $payload, $hash) = $jwt;
+        $payload = Binary\ByteBuffer::fromBase64Url($payload)->getJson();
+
+        $count = 0;
+        if (\is_object($payload) && \property_exists($payload, 'entries') && \is_array($payload->entries)) {
+            foreach ($payload->entries as $entry) {
+                if (\is_object($entry) && \property_exists($entry, 'metadataStatement') && \is_object($entry->metadataStatement)) {
+                    $description = $entry->metadataStatement->description ?? null;
+                    $attestationRootCertificates = $entry->metadataStatement->attestationRootCertificates ?? null;
+
+                    if ($description && $attestationRootCertificates) {
+
+                        // create filename
+                        $certFilename = \preg_replace('/[^a-z0-9]/i', '_', $description);
+                        $certFilename = \trim(\preg_replace('/\_{2,}/i', '_', $certFilename),'_') . '.pem';
+                        $certFilename = \strtolower($certFilename);
+
+                        // add certificate
+                        $certContent = $description . "\n";
+                        $certContent .= \str_repeat('-', \mb_strlen($description)) . "\n";
+
+                        foreach ($attestationRootCertificates as $attestationRootCertificate) {
+                            $count++;
+                            $certContent .= "\n-----BEGIN CERTIFICATE-----\n";
+                            $certContent .= \chunk_split(\trim($attestationRootCertificate), 64, "\n");
+                            $certContent .= "-----END CERTIFICATE-----\n";
+                        }
+
+                        if (\file_put_contents($certFolder . DIRECTORY_SEPARATOR . $certFilename, $certContent) === false) {
+                            throw new WebAuthnException('unable to save certificate from FIDO Alliance Metadata Service');
+                        }
+                    }
+                }
+            }
+        }
+
+        return $count;
+    }
+
     // -----------------------------------------------
     // PRIVATE
     // -----------------------------------------------

+ 1 - 1
data/web/inc/lib/WebAuthn/WebAuthnException.php

@@ -1,5 +1,5 @@
 <?php
-namespace WebAuthn;
+namespace lbuchs\WebAuthn;
 
 /**
  * @author Lukas Buchs

+ 16 - 12
data/web/inc/prerequisites.inc.php

@@ -54,24 +54,28 @@ foreach ($css_dir as $css_file) {
 }
 
 // U2F API + T/HOTP API
+// u2f - deprecated, should be removed
 $u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']);
 $qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
 $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
 
 // FIDO2
 $formats = $GLOBALS['FIDO2_FORMATS'];
-$WebAuthn = new \WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats);
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/solo.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/apple.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/nitro.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/yubico.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/hypersecu.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/globalSign.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/googleHardware.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/microsoftTpmCollection.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/huawei.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/trustkey.pem');
-$WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/bsi.pem');
+$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats);
+// only include root ca's when dev mode is false, to support testing with chromiums virutal authenticator
+if (!$DEV_MODE){
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/solo.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/apple.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/nitro.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/yubico.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/hypersecu.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/globalSign.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/googleHardware.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/microsoftTpmCollection.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/huawei.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/trustkey.pem');
+    $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/bsi.pem');
+}
 
 // Redis
 $redis = new Redis();

+ 15 - 2
data/web/inc/triggers.inc.php

@@ -1,15 +1,28 @@
 <?php
 if (isset($_POST["verify_tfa_login"])) {
-  if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST["token"])) {
+  if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST, $WebAuthn)) {
     $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
     $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
     unset($_SESSION['pending_mailcow_cc_username']);
     unset($_SESSION['pending_mailcow_cc_role']);
     unset($_SESSION['pending_tfa_method']);
-		header("Location: /user");
+	
+    header("Location: /user");
+  } else {
+    unset($_SESSION['pending_mailcow_cc_username']);
+    unset($_SESSION['pending_mailcow_cc_role']);
+    unset($_SESSION['pending_tfa_method']);
   }
 }
 
+if (isset($_GET["cancel_tfa_login"])) {
+    unset($_SESSION['pending_mailcow_cc_username']);
+    unset($_SESSION['pending_mailcow_cc_role']);
+    unset($_SESSION['pending_tfa_method']);
+
+    header("Location: /");
+}
+
 if (isset($_POST["quick_release"])) {
 	quarantine('quick_release', $_POST["quick_release"]);
 }

+ 6 - 0
data/web/inc/vars.inc.php

@@ -192,11 +192,17 @@ $SHOW_LAST_LOGIN = true;
 // true = required
 // false = preferred
 // string 'required' 'preferred' 'discouraged'
+$WEBAUTHN_UV_FLAG_REGISTER = false;
+$WEBAUTHN_UV_FLAG_LOGIN = false:
+$WEBAUTHN_USER_PRESENT_FLAG = true;
+
 $FIDO2_UV_FLAG_REGISTER = 'preferred';
 $FIDO2_UV_FLAG_LOGIN = 'preferred'; // iOS ignores the key via NFC if required - known issue
 $FIDO2_USER_PRESENT_FLAG = true;
+
 $FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm');
 
+
 // Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
 $RSPAMD_MAPS = array(
   'regex' => array(

+ 90 - 17
data/web/json_api.php

@@ -117,12 +117,12 @@ if (isset($_GET['query'])) {
           echo isset($_SESSION['return']) ? json_encode($_SESSION['return']) : $generic_success;
         }
       }
-      if (!isset($_POST['attr']) && $category != "fido2-registration") {
+      if (!isset($_POST['attr']) && $category != "fido2-registration" && $category != "webauthn-tfa-registration") {
         echo $request_incomplete;
         exit;
       }
       else {
-        if ($category != "fido2-registration") {
+        if ($category != "fido2-registration" && $category != "webauthn-tfa-registration") {
           $attr = (array)json_decode($_POST['attr'], true);
         }
         unset($attr['csrf_token']);
@@ -170,6 +170,48 @@ if (isset($_GET['query'])) {
             exit;
           }
         break;
+        case "webauthn-tfa-registration":
+            if (isset($_SESSION["mailcow_cc_role"])) {
+              // parse post data
+              $post = trim(file_get_contents('php://input'));
+              if ($post) $post = json_decode($post);
+
+              // decode base64 strings
+              $clientDataJSON = base64_decode($post->clientDataJSON);
+              $attestationObject = base64_decode($post->attestationObject);             
+              
+              // process registration data from authenticator
+              try {
+                // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
+                $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
+              }
+              catch (Throwable $ex) {
+                // err
+                $return = new stdClass();
+                $return->success = false;
+                $return->msg = $ex->getMessage();
+                echo json_encode($return);
+                exit;
+              }
+
+              // safe authenticator in mysql `tfa` table
+              $_data['tfa_method'] = $post->tfa_method;
+              $_data['key_id'] = $post->key_id;
+              $_data['registration'] = $data;
+              set_tfa($_data);
+
+              // send response
+              $return = new stdClass();
+              $return->success = true;
+              echo json_encode($return);
+              exit;
+            }
+            else {
+              // err - request incomplete
+              echo $request_incomplete;
+              exit;
+            }
+        break;
         case "time_limited_alias":
           process_add_return(mailbox('add', 'time_limited_alias', $attr));
         break;
@@ -350,6 +392,7 @@ if (isset($_GET['query'])) {
         exit();
       }
       switch ($category) {
+        // u2f - deprecated, should be removed
         case "u2f-registration":
           header('Content-Type: application/javascript');
           if (isset($_SESSION["mailcow_cc_role"]) && $_SESSION["mailcow_cc_username"] == $object) {
@@ -366,21 +409,6 @@ if (isset($_GET['query'])) {
             return;
           }
         break;
-        // fido2-registration via GET
-        case "fido2-registration":
-          header('Content-Type: application/json');
-          if (isset($_SESSION["mailcow_cc_role"])) {
-              // Exclude existing CredentialIds, if any
-              $excludeCredentialIds = fido2(array("action" => "get_user_cids"));
-              $createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, true, $GLOBALS['FIDO2_UV_FLAG_REGISTER'], $excludeCredentialIds);
-              print(json_encode($createArgs));
-              $_SESSION['challenge'] = $WebAuthn->getChallenge();
-              return;
-          }
-          else {
-            return;
-          }
-        break;
         case "u2f-authentication":
           header('Content-Type: application/javascript');
           if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
@@ -403,6 +431,21 @@ if (isset($_GET['query'])) {
             return;
           }
         break;
+        // fido2
+        case "fido2-registration":
+          header('Content-Type: application/json');
+          if (isset($_SESSION["mailcow_cc_role"])) {
+              // Exclude existing CredentialIds, if any
+              $excludeCredentialIds = fido2(array("action" => "get_user_cids"));
+              $createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, true, $GLOBALS['FIDO2_UV_FLAG_REGISTER'], $excludeCredentialIds);
+              print(json_encode($createArgs));
+              $_SESSION['challenge'] = $WebAuthn->getChallenge();
+              return;
+          }
+          else {
+            return;
+          }
+        break;
         case "fido2-get-args":
           header('Content-Type: application/json');
           // Login without username, no ids!
@@ -416,6 +459,36 @@ if (isset($_GET['query'])) {
           $_SESSION['challenge'] = $WebAuthn->getChallenge();
           return;
         break;
+        // webauthn two factor authentication
+        case "webauthn-tfa-registration":
+          if (isset($_SESSION["mailcow_cc_role"])) {
+              $excludeCredentialIds = null;
+
+              $createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, false, $GLOBALS['WEBAUTHN_UV_FLAG_REGISTER'], true);
+              
+              print(json_encode($createArgs));
+              $_SESSION['challenge'] = $WebAuthn->getChallenge();
+              return;
+
+          }
+          else {
+            return;
+          }
+        break;
+        case "webauthn-tfa-get-args":
+          $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username");
+          $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username']));
+          $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+          while($row = array_shift($rows)) {
+            $cids[] = base64_decode($row['keyHandle']);
+          }
+
+          $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']);
+          $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId);
+          print(json_encode($getArgs));
+          $_SESSION['challenge'] = $WebAuthn->getChallenge();
+          return;
+        break;
       }
       if (isset($_SESSION['mailcow_cc_role'])) {
         switch ($category) {

+ 3 - 3
data/web/lang/lang.ca.json

@@ -443,9 +443,9 @@
         "set_tfa": "Definir el mètode d'autenticació de dos factors",
         "tfa": "Autenticació de dos factors",
         "totp": "OTP basat en temps (Google Authenticator etc.)",
-        "u2f": "Autenticació U2F",
-        "waiting_usb_auth": "<i>Esperant el dispositiu USB...</i><br><br>Apreta el botó del teu dispositiu USB U2F ara.",
-        "waiting_usb_register": "<i>Esperant el dispositiu USB...</i><br><br>Posa el teu password i confirma el registre del teu U2F apretant el botó del teu dispositiiu USB U2F.",
+        "webauthn": "Autenticació WebAuthn",
+        "waiting_usb_auth": "<i>Esperant el dispositiu USB...</i><br><br>Apreta el botó del teu dispositiu USB WebAuthn ara.",
+        "waiting_usb_register": "<i>Esperant el dispositiu USB...</i><br><br>Posa el teu password i confirma el registre del teu WebAuthn apretant el botó del teu dispositiiu USB WebAuthn.",
         "yubi_otp": "Autenticació OTP de Yubico"
     },
     "user": {

+ 7 - 7
data/web/lang/lang.cs.json

@@ -454,7 +454,7 @@
         "tls_policy_map_parameter_invalid": "Parametr pravidel TLS je neplatný",
         "totp_verification_failed": "TOTP ověření selhalo",
         "transport_dest_exists": "Transportní cíl \"%s\" již existuje",
-        "u2f_verification_failed": "U2F ověření selhalo: %s",
+        "webauthn": "WebAuthn ověření selhalo: %s",
         "unknown": "Došlo k neznámé chybě",
         "unknown_tfa_method": "Neznámá 2FA metoda",
         "unlimited_quota_acl": "Neomeznou kvótu nepovoluje seznam oprávnění ACL",
@@ -972,7 +972,7 @@
         "upload_success": "Soubor úspěšně nahrán",
         "verified_fido2_login": "Ověřené FIDO2 přihlášení",
         "verified_totp_login": "TOTP přihlášení ověřeno",
-        "verified_u2f_login": "U2F přihlášení ověřeno",
+        "webauthn": "WebAuthn přihlášení ověřeno",
         "verified_yotp_login": "Yubico OTP přihlášení ověřeno"
     },
     "tfa": {
@@ -983,7 +983,7 @@
         "disable_tfa": "Zakázat 2FA do příštího úspěšného přihlášení",
         "enter_qr_code": "Kód TOTP, pokud zařízení neumí číst QR kódy",
         "error_code": "Kód chyby",
-        "init_u2f": "Probíhá inicializace, čekejte...",
+        "init_webauthn": "Probíhá inicializace, čekejte...",
         "key_id": "Identifikátor YubiKey",
         "key_id_totp": "Identifikátor klíče",
         "none": "Deaktivovat",
@@ -991,13 +991,13 @@
         "scan_qr_code": "Prosím načtěte následující kód svou aplikací na ověření nebo zadejte kód ručně.",
         "select": "Prosím vyberte...",
         "set_tfa": "Nastavení způsobu dvoufaktorového ověření",
-        "start_u2f_validation": "Zahájit inicializaci",
+        "start_webauthn_validation": "Zahájit inicializaci",
         "tfa": "Dvoufaktorové ověření (TFA)",
         "tfa_token_invalid": "Neplatný TFA token",
         "totp": "Časově založené OTP (Google Authenticator, Authy apod.)",
-        "u2f": "U2F ověření",
-        "waiting_usb_auth": "<i>Čeká se na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na svém U2F USB zařízení.",
-        "waiting_usb_register": "<i>Čeká se na USB zařízení...</i><br><br>Prosím zadejte své heslo výše a potvrďte U2F registraci stiskem tlačítka na svém U2F USB zařízení.",
+        "webauthn": "WebAuthn ověření",
+        "waiting_usb_auth": "<i>Čeká se na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na svém WebAuthn USB zařízení.",
+        "waiting_usb_register": "<i>Čeká se na USB zařízení...</i><br><br>Prosím zadejte své heslo výše a potvrďte WebAuthn registraci stiskem tlačítka na svém WebAuthn USB zařízení.",
         "yubi_otp": "Yubico OTP ověření"
     },
     "user": {

+ 5 - 5
data/web/lang/lang.da.json

@@ -426,7 +426,7 @@
         "tls_policy_map_parameter_invalid": "Politikparameter er ugyldig",
         "totp_verification_failed": "Bekræftelse af TOTP mislykkedes",
         "transport_dest_exists": "Transport destination \"%s\" eksisterer",
-        "u2f_verification_failed": "U2F-bekræftelse mislykkedes: %s",
+        "webauthn_verification_failed": "WebAuthn-bekræftelse mislykkedes: %s",
         "fido2_verification_failed": "Bekræftelse af FIDO2 mislykkedes: %s",
         "unknown": "Der opstod en ukendt fejl",
         "unknown_tfa_method": "Ukendt TFA-metode",
@@ -881,7 +881,7 @@
         "ui_texts": "Gemte ændringer til UI-tekster",
         "upload_success": "Filen blev uploadet",
         "verified_totp_login": "Bekræftet TOTP-login",
-        "verified_u2f_login": "Bekræftet U2F-login",
+        "verified_webauthn_login": "Bekræftet WebAuthn-login",
         "verified_fido2_login": "Bekræftet FIDO2-login",
         "verified_yotp_login": "Bekræftet Yubico OTP-login"
     },
@@ -893,7 +893,7 @@
         "disable_tfa": "Deaktiver TFA indtil næste vellykkede login",
         "enter_qr_code": "Din TOTP kode hvis din enhed ikke kan scanne QR-koder",
         "error_code": "Fejl kode",
-        "init_u2f": "Initialiserer, vent venligst...",
+        "init_webauthn": "Initialiserer, vent venligst...",
         "key_id": "En identifikator til din YubiKey",
         "key_id_totp": "En identifikator for din nøgle",
         "none": "Deaktivere",
@@ -901,11 +901,11 @@
         "scan_qr_code": "Scan venligst følgende kode med din godkendelsesapp, eller indtast koden manuelt.",
         "select": "Vælg venligst",
         "set_tfa": "Set 2-faktor godkendelses metoden",
-        "start_u2f_validation": "Start validering",
+        "start_webauthn_validation": "Start validering",
         "tfa": "2-faktor godkendelse",
         "tfa_token_invalid": "TFA nøgle ugyldig",
         "totp": "Tids-baseret OTP (Google Authenticator, Authy, etc.)",
-        "u2f": "U2F godkendelse",
+        "webauthn": "WebAuthn godkendelse",
         "waiting_usb_auth": "<i>Venter på USB-enhed...</i><br><br>Tryk let på knappen på din USB-enhed nu.",
         "waiting_usb_register": "<i>Venter på USB-enhed...</i><br><br>Indtast din adgangskode ovenfor, og bekræft din registrering ved at trykke på knappen på din USB-enhed.",
         "yubi_otp": "Yubico OTP godkendelse"

+ 5 - 5
data/web/lang/lang.de.json

@@ -455,7 +455,7 @@
         "tls_policy_map_parameter_invalid": "Parameter ist ungültig",
         "totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
         "transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
-        "u2f_verification_failed": "U2F-Verifizierung fehlgeschlagen: %s",
+        "webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
         "unknown": "Ein unbekannter Fehler trat auf",
         "unknown_tfa_method": "Unbekannte TFA-Methode",
         "unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
@@ -971,7 +971,7 @@
         "upload_success": "Datei wurde erfolgreich hochgeladen",
         "verified_fido2_login": "FIDO2-Anmeldung verifiziert",
         "verified_totp_login": "TOTP-Anmeldung verifiziert",
-        "verified_u2f_login": "U2F-Anmeldung verifiziert",
+        "verified_webauthn_login": "WebAuthn-Anmeldung verifiziert",
         "verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
     },
     "tfa": {
@@ -982,7 +982,7 @@
         "disable_tfa": "Deaktiviere 2FA bis zur nächsten erfolgreichen Anmeldung",
         "enter_qr_code": "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel",
         "error_code": "Fehlercode",
-        "init_u2f": "Initialisiere, bitte warten...",
+        "init_webauthn": "Initialisiere, bitte warten...",
         "key_id": "Ein Namen für diesen YubiKey",
         "key_id_totp": "Ein eindeutiger Name",
         "none": "Deaktiviert",
@@ -990,11 +990,11 @@
         "scan_qr_code": "Bitte scannen Sie jetzt den angezeigten QR-Code:",
         "select": "Bitte auswählen",
         "set_tfa": "Konfiguriere Zwei-Faktor-Authentifizierungsmethode",
-        "start_u2f_validation": "Starte Validierung",
+        "start_webauthn_validation": "Starte Validierung",
         "tfa": "Zwei-Faktor-Authentifizierung",
         "tfa_token_invalid": "TFA-Token ungültig!",
         "totp": "Time-based-OTP (Google Authenticator etc.)",
-        "u2f": "U2F-Authentifizierung",
+        "webauthn": "WebAuthn-Authentifizierung",
         "waiting_usb_auth": "<i>Warte auf USB-Gerät...</i><br><br>Bitte jetzt den vorgesehenen Taster des USB-Gerätes berühren.",
         "waiting_usb_register": "<i>Warte auf USB-Gerät...</i><br><br>Bitte zuerst das obere Passwortfeld ausfüllen und erst dann den vorgesehenen Taster des USB-Gerätes berühren.",
         "yubi_otp": "Yubico OTP-Authentifizierung"

+ 5 - 5
data/web/lang/lang.en.json

@@ -455,7 +455,7 @@
         "tls_policy_map_parameter_invalid": "Policy parameter is invalid",
         "totp_verification_failed": "TOTP verification failed",
         "transport_dest_exists": "Transport destination \"%s\" exists",
-        "u2f_verification_failed": "U2F verification failed: %s",
+        "webauthn_verification_failed": "WebAuthn verification failed: %s",
         "unknown": "An unknown error occurred",
         "unknown_tfa_method": "Unknown TFA method",
         "unlimited_quota_acl": "Unlimited quota prohibited by ACL",
@@ -978,7 +978,7 @@
         "upload_success": "File uploaded successfully",
         "verified_fido2_login": "Verified FIDO2 login",
         "verified_totp_login": "Verified TOTP login",
-        "verified_u2f_login": "Verified U2F login",
+        "verified_webauthn_login": "Verified WebAuthn login",
         "verified_yotp_login": "Verified Yubico OTP login"
     },
     "tfa": {
@@ -989,7 +989,7 @@
         "disable_tfa": "Disable TFA until next successful login",
         "enter_qr_code": "Your TOTP code if your device cannot scan QR codes",
         "error_code": "Error code",
-        "init_u2f": "Initializing, please wait...",
+        "init_webauthn": "Initializing, please wait...",
         "key_id": "An identifier for your YubiKey",
         "key_id_totp": "An identifier for your key",
         "none": "Deactivate",
@@ -997,11 +997,11 @@
         "scan_qr_code": "Please scan the following code with your authenticator app or enter the code manually.",
         "select": "Please select",
         "set_tfa": "Set two-factor authentication method",
-        "start_u2f_validation": "Start validation",
+        "start_webauthn_validation": "Start validation",
         "tfa": "Two-factor authentication",
         "tfa_token_invalid": "TFA token invalid",
         "totp": "Time-based OTP (Google Authenticator, Authy, etc.)",
-        "u2f": "U2F authentication",
+        "webauthn": "WebAuthn authentication",
         "waiting_usb_auth": "<i>Waiting for USB device...</i><br><br>Please tap the button on your USB device now.",
         "waiting_usb_register": "<i>Waiting for USB device...</i><br><br>Please enter your password above and confirm your registration by tapping the button on your USB device.",
         "yubi_otp": "Yubico OTP authentication"

+ 5 - 5
data/web/lang/lang.es.json

@@ -323,7 +323,7 @@
         "tls_policy_map_parameter_invalid": "El parámetro de póliza no es válido.",
         "totp_verification_failed": "Verificación TOTP fallida",
         "transport_dest_exists": "Destino de la regla de transporte ya existe",
-        "u2f_verification_failed": "Verificación U2F fallida: %s",
+        "webauthn_verification_failed": "Verificación WebAuthn fallida: %s",
         "unknown": "Se produjo un error desconocido",
         "unknown_tfa_method": "Método TFA desconocido",
         "unlimited_quota_acl": "Cuota ilimitada restringida por controles administrativos",
@@ -649,7 +649,7 @@
         "tls_policy_map_entry_deleted": "Regla de póliza de TLS con ID %s ha sido elimindada",
         "tls_policy_map_entry_saved": "Regla de póliza de TLS \"%s\" ha sido guardada",
         "verified_totp_login": "Inicio de sesión TOTP verificado",
-        "verified_u2f_login": "Inicio de sesión U2F verificado",
+        "verified_webauthn_login": "Inicio de sesión WebAuthn verificado",
         "verified_yotp_login": "Inicio de sesión Yubico OTP verificado"
     },
     "tfa": {
@@ -667,9 +667,9 @@
         "set_tfa": "Establecer el método de autenticación de dos factores",
         "tfa": "Autenticación de dos factores",
         "totp": "OTP basado en tiempo (Google Authenticator, Authy, etc.)",
-        "u2f": "Autenticación U2F",
-        "waiting_usb_auth": "<i>Esperando al dispositivo USB...</i><br><br>Toque el botón en su dispositivo USB U2F ahora.",
-        "waiting_usb_register": "<i>Esperando al dispositivo USB....</i><br><br>Ingrese su contraseña arriba y confirme su registro U2F tocando el botón en su dispositivo USB U2F.",
+        "webauthn": "Autenticación WebAuthn",
+        "waiting_usb_auth": "<i>Esperando al dispositivo USB...</i><br><br>Toque el botón en su dispositivo USB WebAuthn ahora.",
+        "waiting_usb_register": "<i>Esperando al dispositivo USB....</i><br><br>Ingrese su contraseña arriba y confirme su registro WebAuthn tocando el botón en su dispositivo USB WebAuthn.",
         "yubi_otp": "Yubico OTP"
     },
     "user": {

+ 7 - 7
data/web/lang/lang.fi.json

@@ -370,7 +370,7 @@
         "tls_policy_map_parameter_invalid": "Käytäntö parametri ei kelpaa",
         "totp_verification_failed": "TOTP-vahvistus epäonnistui",
         "transport_dest_exists": "Kuljetuksen määränpää \"%s\" olemassa",
-        "u2f_verification_failed": "U2F vahvistaminen epäonnistui: %s",
+        "webauthn_verification_failed": "WebAuthn vahvistaminen epäonnistui: %s",
         "unknown": "Ilmeni tuntematon virhe",
         "unknown_tfa_method": "Tuntematon TFA-menetelmä",
         "unlimited_quota_acl": "Rajoittamaton kiintiö kielletty ACL",
@@ -754,7 +754,7 @@
         "ui_texts": "Tallennettu käyttöliittymätekstien muutokset",
         "upload_success": "Tiedosto ladattu onnistuneesti",
         "verified_totp_login": "Vahvistettu TOTP-kirjautuminen",
-        "verified_u2f_login": "Vahvistettu U2F kirjautuminen",
+        "verified_webauthn_login": "Vahvistettu WebAuthn kirjautuminen",
         "verified_yotp_login": "Vahvistettu Yubico OTP kirjautuminen"
     },
     "tfa": {
@@ -765,7 +765,7 @@
         "disable_tfa": "Poista TFA käytöstä seuraavaan onnistuneen kirjautumisen jälkeen",
         "enter_qr_code": "TOTP-koodisi, jos laitteesi ei pysty tarkistamaan QR-koodeja",
         "error_code": "Virhekoodi",
-        "init_u2f": "Alustetaan, odota...",
+        "init_webauthn": "Alustetaan, odota...",
         "key_id": "Tunniste YubiKey",
         "key_id_totp": "Avaimen tunnus",
         "none": "Poista",
@@ -773,12 +773,12 @@
         "scan_qr_code": "Tarkista seuraava koodi Authenticator-sovelluksella tai Syötä koodi manuaalisesti.",
         "select": "Valitse",
         "set_tfa": "Määritä kaksiosainen todennus menetelmä",
-        "start_u2f_validation": "Aloita oikeellisuus tarkistus",
+        "start_webauthn_validation": "Aloita oikeellisuus tarkistus",
         "tfa": "Kaksiosainen todennus",
         "totp": "Aikapohjainen OTP (Google Authenticator, Authy jne.)",
-        "u2f": "U2F todennus",
-        "waiting_usb_auth": "<i>Odotetaan USB-laitetta...</i><br><br>Napauta painiketta U2F USB-laitteessa nyt",
-        "waiting_usb_register": "<i>Odotetaan USB-laitetta...</i><br><br>Anna salasanasi yltä ja vahvista U2F-rekisteröinti napauttamalla painiketta U2F USB-laitteessa.",
+        "webauthn": "WebAuthn todennus",
+        "waiting_usb_auth": "<i>Odotetaan USB-laitetta...</i><br><br>Napauta painiketta WebAuthn USB-laitteessa nyt",
+        "waiting_usb_register": "<i>Odotetaan USB-laitetta...</i><br><br>Anna salasanasi yltä ja vahvista WebAuthn-rekisteröinti napauttamalla painiketta WebAuthn USB-laitteessa.",
         "yubi_otp": "Yubico OTP-todennus"
     },
     "user": {

+ 7 - 7
data/web/lang/lang.fr.json

@@ -430,7 +430,7 @@
         "tls_policy_map_parameter_invalid": "Le paramètre Policy est invalide",
         "totp_verification_failed": "Echec de la vérification TOTP",
         "transport_dest_exists": "La destination de transport \"%s\" existe",
-        "u2f_verification_failed": "Echec de la vérification U2F: %s",
+        "webauthn_verification_failed": "Echec de la vérification WebAuthn: %s",
         "fido2_verification_failed": "La vérification FIDO2 a échoué: %s",
         "unknown": "Une erreur inconnue est survenue",
         "unknown_tfa_method": "Methode TFA inconnue",
@@ -895,7 +895,7 @@
         "ui_texts": "Enregistrement des modifications apportées aux textes de l’interface utilisateur",
         "upload_success": "Fichier téléchargé avec succès",
         "verified_totp_login": "Authentification TOTP vérifiée",
-        "verified_u2f_login": "Authentification U2F vérifiée",
+        "verified_webauthn_login": "Authentification WebAuthn vérifiée",
         "verified_fido2_login": "Authentification FIDO2 vérifiée",
         "verified_yotp_login": "Authentification Yubico OTP vérifiée"
     },
@@ -907,7 +907,7 @@
         "disable_tfa": "Désactiver TFA jusqu’à la prochaine ouverture de session réussie",
         "enter_qr_code": "Votre code TOTP si votre appareil ne peut pas scanner les codes QR",
         "error_code": "Code d'erreur",
-        "init_u2f": "Initialisation, veuillez patienter...",
+        "init_webauthn": "Initialisation, veuillez patienter...",
         "key_id": "Un identifiant pour votre Yubikey",
         "key_id_totp": "Un identifiant pour votre clé",
         "none": "Désactiver",
@@ -915,13 +915,13 @@
         "scan_qr_code": "Veuillez scanner le code suivant avec votre application d’authentification ou entrer le code manuellement.",
         "select": "Veuillez sélectionner",
         "set_tfa": "Définir une méthode d’authentification à deux facteurs",
-        "start_u2f_validation": "Début de la validation",
+        "start_webauthn_validation": "Début de la validation",
         "tfa": "Authentification à deux facteurs",
         "tfa_token_invalid": "Token TFA invalide",
         "totp": "OTP (One Time Password = Mot de passe à usage unique : Google Authenticator, Authy, etc.)",
-        "u2f": "Authentification U2F",
-        "waiting_usb_auth": "<i>En attente d’un périphérique USB...</i><br><br>S’il vous plaît appuyez maintenant sur le bouton de votre périphérique USB U2F.",
-        "waiting_usb_register": "<i>En attente d’un périphérique USB...</i><br><br>Veuillez entrer votre mot de passe ci-dessus et confirmer votre inscription U2F en appuyant sur le bouton de votre périphérique USB U2F.",
+        "webauthn": "Authentification WebAuthn",
+        "waiting_usb_auth": "<i>En attente d’un périphérique USB...</i><br><br>S’il vous plaît appuyez maintenant sur le bouton de votre périphérique USB WebAuthn.",
+        "waiting_usb_register": "<i>En attente d’un périphérique USB...</i><br><br>Veuillez entrer votre mot de passe ci-dessus et confirmer votre inscription WebAuthn en appuyant sur le bouton de votre périphérique USB WebAuthn.",
         "yubi_otp": "Authentification OTP Yubico"
     },
     "fido2": {

+ 7 - 7
data/web/lang/lang.it.json

@@ -449,7 +449,7 @@
         "tls_policy_map_parameter_invalid": "Policy parameter is invalid",
         "totp_verification_failed": "TOTP verification failed",
         "transport_dest_exists": "Transport destination \"%s\" exists",
-        "u2f_verification_failed": "U2F verification failed: %s",
+        "webauthn_verification_failed": "WebAuthn verification failed: %s",
         "unknown": "An unknown error occurred",
         "unknown_tfa_method": "Unknown TFA method",
         "unlimited_quota_acl": "Unlimited quota prohibited by ACL",
@@ -947,7 +947,7 @@
         "upload_success": "File caricato con successo",
         "verified_fido2_login": "Verified FIDO2 login",
         "verified_totp_login": "Verified TOTP login",
-        "verified_u2f_login": "Verified U2F login",
+        "verified_webauthn_login": "Verified WebAuthn login",
         "verified_yotp_login": "Verified Yubico OTP login"
     },
     "tfa": {
@@ -958,7 +958,7 @@
         "disable_tfa": "Disabilita TFA fino al prossimo accesso",
         "enter_qr_code": "Il codice TOTP se il tuo dispositivo non è in grado di acquisire i codici QR",
         "error_code": "Codice di errore",
-        "init_u2f": "Inizializzazione, attendere prego...",
+        "init_webauthn": "Inizializzazione, attendere prego...",
         "key_id": "Identificatore per il tuo YubiKey",
         "key_id_totp": "Identificatore per la tua chiave",
         "none": "Disattivato",
@@ -966,12 +966,12 @@
         "scan_qr_code": "Esegui la scansione del seguente codice con l'applicazione di autenticazione o inserisci manualmente il codice.",
         "select": "Seleziona",
         "set_tfa": "Imposta il metodo di autenticazione a due fattori",
-        "start_u2f_validation": "Avvia convalida",
+        "start_webauthn_validation": "Avvia convalida",
         "tfa": "Autenticazione a due fattori",
         "totp": "Time-based OTP (Google Authenticator etc.)",
-        "u2f": "Autenticazione U2F",
-        "waiting_usb_auth": "<i>In attesa del device USB...</i><br /><br />Tocca ora il pulsante sul dispositivo U2F USB.",
-        "waiting_usb_register": "<i>In attesa del device USB...</i><br /><br />Inserisci la tua password qui sopra e conferma la tua registrazione U2F toccando il pulsante del dispositivo U2F USB.",
+        "webauthn": "Autenticazione WebAuthn",
+        "waiting_usb_auth": "<i>In attesa del device USB...</i><br /><br />Tocca ora il pulsante sul dispositivo WebAuthn USB.",
+        "waiting_usb_register": "<i>In attesa del device USB...</i><br /><br />Inserisci la tua password qui sopra e conferma la tua registrazione WebAuthn toccando il pulsante del dispositivo WebAuthn USB.",
         "yubi_otp": "Autenticazione Yubico OTP",
         "tfa_token_invalid": "Token TFA non valido"
     },

+ 7 - 7
data/web/lang/lang.ko.json

@@ -417,7 +417,7 @@
         "tls_policy_map_parameter_invalid": "유효하지 않은 정책 매개변수",
         "totp_verification_failed": "TOTP 확인 실패",
         "transport_dest_exists": "전송 목적지 \"%s\"가 존재합니다.",
-        "u2f_verification_failed": "U2F 검증 실패: %s",
+        "webauthn_verification_failed": "WebAuthn 검증 실패: %s",
         "unknown": "알 수 없는 오류 발생",
         "unknown_tfa_method": "알 수 없는 TFA 방식",
         "unlimited_quota_acl": "ACL에 따라 할당량을 무제한으로 둘 수 없습니다.",
@@ -852,7 +852,7 @@
         "ui_texts": "Saved changes to UI texts",
         "upload_success": "File uploaded successfully",
         "verified_totp_login": "Verified TOTP login",
-        "verified_u2f_login": "Verified U2F login",
+        "verified_webauthn_login": "Verified WebAuthn login",
         "verified_yotp_login": "Verified Yubico OTP login"
     },
     "tfa": {
@@ -863,7 +863,7 @@
         "disable_tfa": "Disable TFA until next successful login",
         "enter_qr_code": "Your TOTP code if your device cannot scan QR codes",
         "error_code": "Error code",
-        "init_u2f": "Initializing, please wait...",
+        "init_webauthn": "Initializing, please wait...",
         "key_id": "An identifier for your YubiKey",
         "key_id_totp": "An identifier for your key",
         "none": "Deactivate",
@@ -871,12 +871,12 @@
         "scan_qr_code": "Please scan the following code with your authenticator app or enter the code manually.",
         "select": "Please select",
         "set_tfa": "Set two-factor authentication method",
-        "start_u2f_validation": "Start validation",
+        "start_webauthn_validation": "Start validation",
         "tfa": "Two-factor authentication",
         "totp": "Time-based OTP (Google Authenticator, Authy, etc.)",
-        "u2f": "U2F authentication",
-        "waiting_usb_auth": "<i>Waiting for USB device...</i><br><br>Please tap the button on your U2F USB device now.",
-        "waiting_usb_register": "<i>Waiting for USB device...</i><br><br>Please enter your password above and confirm your U2F registration by tapping the button on your U2F USB device.",
+        "webauthn": "WebAuthn authentication",
+        "waiting_usb_auth": "<i>Waiting for USB device...</i><br><br>Please tap the button on your WebAuthn USB device now.",
+        "waiting_usb_register": "<i>Waiting for USB device...</i><br><br>Please enter your password above and confirm your WebAuthn registration by tapping the button on your WebAuthn USB device.",
         "yubi_otp": "Yubico OTP authentication"
     },
     "user": {

+ 3 - 3
data/web/lang/lang.lv.json

@@ -450,9 +450,9 @@
         "set_tfa": "Uzstādīt difi faktoru autentifik;acijas metodi",
         "tfa": "Divu faktoru autentifikācija",
         "totp": "Uz laiku bāzēta vienreizēja parole (Google Autentifikātors utt.)",
-        "u2f": "U2F autentifikācija",
-        "waiting_usb_auth": "<i>Gaida USB ierīci...</i><br><br>Lūdzu, tagad nospiežiet pogu uz Jūsu U2F USB ierīces.",
-        "waiting_usb_register": "<i>Gaida USB ierīci...</i><br><br>Lūdzu augšā ievadiet Jūsu paroli un apstipriniet U2F reģistrāciju nospiežot pogu uz Jūsu U2F USB ierīces.",
+        "webauthn": "WebAuthn autentifikācija",
+        "waiting_usb_auth": "<i>Gaida USB ierīci...</i><br><br>Lūdzu, tagad nospiežiet pogu uz Jūsu WebAuthn USB ierīces.",
+        "waiting_usb_register": "<i>Gaida USB ierīci...</i><br><br>Lūdzu augšā ievadiet Jūsu paroli un apstipriniet WebAuthn reģistrāciju nospiežot pogu uz Jūsu WebAuthn USB ierīces.",
         "yubi_otp": "Yubico OTP autentifikators"
     },
     "user": {

+ 7 - 7
data/web/lang/lang.nl.json

@@ -428,7 +428,7 @@
         "tls_policy_map_parameter_invalid": "Beleidsparameter is ongeldig",
         "totp_verification_failed": "TOTP-verificatie mislukt",
         "transport_dest_exists": "Transportbestemming \"%s\" bestaat reeds",
-        "u2f_verification_failed": "U2F-verificatie mislukt: %s",
+        "webauthn_verification_failed": "WebAuthn-verificatie mislukt: %s",
         "fido2_verification_failed": "FIDO2-verificatie mislukt: %s",
         "unknown": "Er is een onbekende fout opgetreden",
         "unknown_tfa_method": "Onbekende tweefactorauthenticatiemethode",
@@ -891,7 +891,7 @@
         "ui_texts": "Wijzigingen aan labels en teksten zijn opgeslagen",
         "upload_success": "Bestand succesvol geupload",
         "verified_totp_login": "TOTP succesvol geverifieerd",
-        "verified_u2f_login": "U2F succesvol geverifieerd",
+        "verified_webauthn_login": "WebAuthn succesvol geverifieerd",
         "verified_fido2_login": "FIDO2 succesvol geverifieerd",
         "verified_yotp_login": "Yubico OTP succesvol geverifieerd"
     },
@@ -903,7 +903,7 @@
         "disable_tfa": "Pauzeer tweefactorauthenticatie tot de eerstvolgende succesvolle login",
         "enter_qr_code": "Voer deze code in als je apparaat geen QR-codes kan scannen:",
         "error_code": "Errorcode",
-        "init_u2f": "Even geduld aub...",
+        "init_webauthn": "Even geduld aub...",
         "key_id": "Geef deze YubiKey een naam",
         "key_id_totp": "Geef deze key een naam",
         "none": "Deactiveer",
@@ -911,13 +911,13 @@
         "scan_qr_code": "Scan de volgende QR-code met je authenticatie-app:",
         "select": "Selecteer...",
         "set_tfa": "Kies methode voor tweefactorauthenticatie",
-        "start_u2f_validation": "Start validatie",
+        "start_webauthn_validation": "Start validatie",
         "tfa": "Tweefactorauthenticatie",
         "tfa_token_invalid": "Tweefactorauthenticatietoken is ongeldig",
         "totp": "TOTP (Step Two, Authy, etc.)",
-        "u2f": "U2F",
-        "waiting_usb_auth": "<i>In afwachting van USB-apparaat...</i><br><br>Druk nu op de knop van je U2F-apparaat.",
-        "waiting_usb_register": "<i>In afwachting van USB-apparaat...</i><br><br>Voer je wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken.",
+        "webauthn": "WebAuthn",
+        "waiting_usb_auth": "<i>In afwachting van USB-apparaat...</i><br><br>Druk nu op de knop van je WebAuthn-apparaat.",
+        "waiting_usb_register": "<i>In afwachting van USB-apparaat...</i><br><br>Voer je wachtwoord hierboven in en bevestig de registratie van het WebAuthn-apparaat door op de knop van het apparaat te drukken.",
         "yubi_otp": "Yubico OTP"
     },
     "fido2": {

+ 3 - 3
data/web/lang/lang.pl.json

@@ -329,9 +329,9 @@
         "set_tfa": "Ustaw metodę uwierzytelniania dwuetapowego",
         "tfa": "Uwierzytelnianie dwuetapowe",
         "totp": "Time-based OTP (Google Authenticator itd.)",
-        "u2f": "Uwierzytelnianie U2F",
-        "waiting_usb_auth": "<i>Czekam na urządzenie USB...</i><br><br>Wciśnij teraz przycisk na urządzeniu U2F USB.",
-        "waiting_usb_register": "<i> Czekam na urządzenie USB...</i><br><br>Wprowadź swoje  hasło powyżej i potwierdź rejestrację U2F przez naciśnięcie przycisku na urządzeniu U2F USB.",
+        "webauthn": "Uwierzytelnianie WebAuthn",
+        "waiting_usb_auth": "<i>Czekam na urządzenie USB...</i><br><br>Wciśnij teraz przycisk na urządzeniu WebAuthn USB.",
+        "waiting_usb_register": "<i> Czekam na urządzenie USB...</i><br><br>Wprowadź swoje  hasło powyżej i potwierdź rejestrację WebAuthn przez naciśnięcie przycisku na urządzeniu WebAuthn USB.",
         "yubi_otp": "Uwierzytelnianie Yubico OTP"
     },
     "user": {

+ 7 - 7
data/web/lang/lang.ro.json

@@ -448,7 +448,7 @@
         "tls_policy_map_parameter_invalid": "Parametrul politicii este invalid",
         "totp_verification_failed": "Verificarea TOTP a eșuat",
         "transport_dest_exists": "Destinația transportului \"%s\" există",
-        "u2f_verification_failed": "Verificarea U2F a eșuat: %s",
+        "webauthn_verification_failed": "Verificarea WebAuthn a eșuat: %s",
         "fido2_verification_failed": "Verificarea FIDO2 a eșuat: %s",
         "unknown": "A apărut o eroare necunoscută",
         "unknown_tfa_method": "Metodă TFA necunoscută",
@@ -928,7 +928,7 @@
         "ui_texts": "Modificări salvate în textele UI",
         "upload_success": "Fișier încărcat cu succes",
         "verified_totp_login": "Autentificarea TOTP verificată",
-        "verified_u2f_login": "Autentificarea U2F verificată",
+        "verified_webauthn_login": "Autentificarea WebAuthn verificată",
         "verified_fido2_login": "Conectare FIDO2 verificată",
         "verified_yotp_login": "Autentificarea Yubico OTP verificată"
     },
@@ -940,7 +940,7 @@
         "disable_tfa": "Dezactivează TFA până la următoarea conectare reușită",
         "enter_qr_code": "Codul tău TOTP dacă dispozitivul tău nu poate scana codurile QR",
         "error_code": "Cod de eroare",
-        "init_u2f": "Inițializare, vă rugăm așteptați...",
+        "init_webauthn": "Inițializare, vă rugăm așteptați...",
         "key_id": "Un identificator pentru YubiKey",
         "key_id_totp": "Un identificator pentru cheia ta",
         "none": "Dezactivează",
@@ -948,13 +948,13 @@
         "scan_qr_code": "Scanează codul următor cu aplicația ta de autentificare sau introdu manual codul.",
         "select": "Te rog selectează",
         "set_tfa": "Setează metoda de autentificare cu doi factori",
-        "start_u2f_validation": "Începi validarea",
+        "start_webauthn_validation": "Începi validarea",
         "tfa": "Autentificare cu doi factori",
         "tfa_token_invalid": "Jeton TFA invalid",
         "totp": "OTP pe bază de timp (Google Authenticator etc.)",
-        "u2f": "Autentificare U2F",
-        "waiting_usb_auth": "<i>În așteptarea dispozitivului USB...</i><br><br>Apasă acum butonul de pe dispozitivul tău USB U2F.",
-        "waiting_usb_register": "<i>În așteptarea dispozitivului USB...</i><br><br>Introdu parola ta mai sus și confirmă înregistrarea ta U2F atingând butonul de pe dispozitivul tău USB U2F.",
+        "webauthn": "Autentificare WebAuthn",
+        "waiting_usb_auth": "<i>În așteptarea dispozitivului USB...</i><br><br>Apasă acum butonul de pe dispozitivul tău USB WebAuthn.",
+        "waiting_usb_register": "<i>În așteptarea dispozitivului USB...</i><br><br>Introdu parola ta mai sus și confirmă înregistrarea ta WebAuthn atingând butonul de pe dispozitivul tău USB WebAuthn.",
         "yubi_otp": "Autentificare Yubico OTP"
     },
     "fido2": {

+ 5 - 5
data/web/lang/lang.ru.json

@@ -454,7 +454,7 @@
         "tls_policy_map_parameter_invalid": "Недопустимое значение параметра политики",
         "totp_verification_failed": "Ошибка валидации TOTP",
         "transport_dest_exists": "Назначение для отправки \"%s\" уже существует",
-        "u2f_verification_failed": "Ошибка валидации U2F: %s",
+        "webauthn_verification_failed": "Ошибка валидации WebAuthn: %s",
         "unknown": "Произошла неизвестная ошибка",
         "unknown_tfa_method": "Неизвестный метод TFA",
         "unlimited_quota_acl": "Неограниченная квота запрещена политикой доступа",
@@ -973,7 +973,7 @@
         "upload_success": "Файл загружен успешно",
         "verified_fido2_login": "Авторизация FIDO2 пройдена",
         "verified_totp_login": "Авторизация TOTP пройдена",
-        "verified_u2f_login": "Авторизация U2F пройдена",
+        "verified_webauthn_login": "Авторизация WebAuthn пройдена",
         "verified_yotp_login": "Авторизация Yubico OTP пройдена"
     },
     "tfa": {
@@ -984,7 +984,7 @@
         "disable_tfa": "Отключить TFA до следующего успешного входа",
         "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код",
         "error_code": "Код ошибки",
-        "init_u2f": "Инициализация, пожалуйста, подождите...",
+        "init_webauthn": "Инициализация, пожалуйста, подождите...",
         "key_id": "Идентификатор YubiKey ключа",
         "key_id_totp": "Идентификатор TOTP ключа",
         "none": "Отключить",
@@ -992,11 +992,11 @@
         "scan_qr_code": "Пожалуйста, отсканируйте QR-код с помощью приложения или введите его вручную.",
         "select": "Пожалуйста, выберите",
         "set_tfa": "Задать метод двухфакторной проверки",
-        "start_u2f_validation": "Начать проверку",
+        "start_webauthn_validation": "Начать проверку",
         "tfa": "Двухфакторная проверка подлинности",
         "tfa_token_invalid": "Неправильный TFA токен",
         "totp": "OTP (Authy, Google Authenticator и др.)",
-        "u2f": "U2F аутентификация",
+        "webauthn": "WebAuthn аутентификация",
         "waiting_usb_auth": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.",
         "waiting_usb_register": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.",
         "yubi_otp": "Yubico OTP аутентификация"

+ 5 - 5
data/web/lang/lang.sk.json

@@ -454,7 +454,7 @@
         "tls_policy_map_parameter_invalid": "Podmienkový parameter mapy TLS pravidiel je neplatný",
         "totp_verification_failed": "TOTP overenie zlyhalo",
         "transport_dest_exists": "Transportný cieľ \"%s\" už existuje",
-        "u2f_verification_failed": "U2F overenie zlyhalo: %s",
+        "webauthn_verification_failed": "WebAuthn overenie zlyhalo: %s",
         "unknown": "Nastala neznáma chyba",
         "unknown_tfa_method": "Neznáma TFA metóda",
         "unlimited_quota_acl": "Neobmedzené kvóta je zakázaná cez ACL",
@@ -973,7 +973,7 @@
         "upload_success": "Súbor úspešne nahratý",
         "verified_fido2_login": "Overené FIDO2 prihlásenie",
         "verified_totp_login": "Overené TOTP prihlásenie",
-        "verified_u2f_login": "Overené U2F prihlásenie",
+        "verified_webauthn_login": "Overené WebAuthn prihlásenie",
         "verified_yotp_login": "Overené Yubico OTP prihlásenie"
     },
     "tfa": {
@@ -984,7 +984,7 @@
         "disable_tfa": "Vypnúť TFA do ďalšieho úspešného prihlásenia",
         "enter_qr_code": "Zadajte váš TOTP kód, ak vaše zariadenie nedokáže skenovať QR kódy",
         "error_code": "Chyba kódu",
-        "init_u2f": "Inicializácia, prosím čakajte...",
+        "init_webauthn": "Inicializácia, prosím čakajte...",
         "key_id": "Identifikátor pre váš YubiKey",
         "key_id_totp": "Identifikátor pre váš kľúč",
         "none": "Deaktivovať",
@@ -992,11 +992,11 @@
         "scan_qr_code": "Prosím oskenujte nasledovný kód pomocou vašej autentizačnej aplikácie alebo zadajte kód manuálne.",
         "select": "Prosím vyberte",
         "set_tfa": "Nastaviť dvojúrovňovú autentifikačnú metódu",
-        "start_u2f_validation": "Spustiť validáciu",
+        "start_webauthn_validation": "Spustiť validáciu",
         "tfa": "Dvojúrovňová autentifikácia (TFA)",
         "tfa_token_invalid": "Neplatný TFA token",
         "totp": "Časovo-založený OTP (Google Authenticator, Authy, atď.)",
-        "u2f": "U2F autentifikácia",
+        "webauthn": "WebAuthn autentifikácia",
         "waiting_usb_auth": "<i>Čakanie na USB zariadenie...</i><br><br>Prosím stlačte tlačidlo na vašom USB zariadení.",
         "waiting_usb_register": "<i>Čakanie na USB zariadenie...</i><br><br>Prosím zadajte vaše heslo a potvrďte registráciu stlačením tlačidla na vašom USB zariadení.",
         "yubi_otp": "Yubico OTP autentifikácia"

+ 6 - 6
data/web/lang/lang.sv.json

@@ -441,8 +441,8 @@
         "tls_policy_map_parameter_invalid": "Policy parameter är ogiltig",
         "totp_verification_failed": "TOTP-verifiering misslyckades",
         "transport_dest_exists": "Transportdestinationen \"%s\" existerar redan",
-        "u2f_verification_failed": "U2F-verifiering misslyckades: %s",
-        "fido2_verification_failed": "U2F-verifiering misslyckades: %s",
+        "webauthn_verification_failed": "WebAuthn-verifiering misslyckades: %s",
+        "fido2_verification_failed": "WebAuthn-verifiering misslyckades: %s",
         "unknown": "Ett fel har inträffat",
         "unknown_tfa_method": "Okänd TFA method",
         "unlimited_quota_acl": "På grund av en åtkomstlista tillåts inte en obegränsad kvot",
@@ -911,7 +911,7 @@
         "ui_texts": "Ändringarna på texter och rubriker i gränssnittet sparade",
         "upload_success": "Filen har laddats upp",
         "verified_totp_login": "Verifierad TOTP inloggning",
-        "verified_u2f_login": "Verifierad U2F inloggning",
+        "verified_webauthn_login": "Verifierad WebAuthn inloggning",
         "verified_fido2_login": "Verifierad FIDO2 inloggning",
         "verified_yotp_login": "Verifierad Yubico OTP inloggning"
     },
@@ -923,7 +923,7 @@
         "disable_tfa": "Inaktivera tvåfaktorsautentisering tills nästa lyckade inloggning",
         "enter_qr_code": "Om du inte kan skanna den QR-kod som visas, använd säkerhetsnyckeln som visas nedan",
         "error_code": "Felkod",
-        "init_u2f": "Initierar, vänta...",
+        "init_webauthn": "Initierar, vänta...",
         "key_id": "En identifierare för din YubiKey",
         "key_id_totp": "En identifierare för din nyckel",
         "none": "Avaktivera",
@@ -931,11 +931,11 @@
         "scan_qr_code": "Skanna nu den QR-kod som visas på skärmen.",
         "select": "Välj",
         "set_tfa": "Metod för tvåfaktorsautentisering",
-        "start_u2f_validation": "Startar validering",
+        "start_webauthn_validation": "Startar validering",
         "tfa": "Tvåfaktorsautentisering",
         "tfa_token_invalid": "TFA nyckeln är ogiltig!",
         "totp": "Tidsbaserad OTP (Google Authenticator, Authy, mm)",
-        "u2f": "U2F-autentisering",
+        "webauthn": "WebAuthn-autentisering",
         "waiting_usb_auth": "<i>Väntar på USB-enhet...</i><br><br>Tryck på knappen på USB-enheten nu.",
         "waiting_usb_register": "<i>Väntar på USB-enhet...</i><br><br>Vänligen fyll i det övre lösenordsfältet först och tryck sedan på knappen på USB-enheten.",
         "yubi_otp": "Yubico OTP-autentisering"

+ 7 - 7
data/web/lang/lang.zh.json

@@ -424,7 +424,7 @@
         "tls_policy_map_parameter_invalid": "策略参数非法",
         "totp_verification_failed": "TOTP认证失败",
         "transport_dest_exists": "传输目标 \"%s\" 已存在",
-        "u2f_verification_failed": "U2F认证失败: %s",
+        "webauthn_verification_failed": "WebAuthn认证失败: %s",
         "unknown": "发生未知错误",
         "unknown_tfa_method": "未知TFA方法",
         "unlimited_quota_acl": "ACL设置禁止了无限配额",
@@ -875,7 +875,7 @@
         "ui_texts": "已保存UI文本更改",
         "upload_success": "成功上传文件",
         "verified_totp_login": "TOTP登录验证成功",
-        "verified_u2f_login": "U2F登录验证成功",
+        "verified_webauthn_login": "WebAuthn登录验证成功",
         "verified_yotp_login": "Yubico OTP登录验证成功"
     },
     "tfa": {
@@ -886,7 +886,7 @@
         "disable_tfa": "在下一次成功登录前关闭两步验证",
         "enter_qr_code": "如果你的设备不能扫描QR码,输入此TOTP码",
         "error_code": "错误码",
-        "init_u2f": "初始化中,请等待...",
+        "init_webauthn": "初始化中,请等待...",
         "key_id": "你的YubiKey的标识",
         "key_id_totp": "你的密钥的标识",
         "none": "禁用",
@@ -894,12 +894,12 @@
         "scan_qr_code": "请用你认证应用扫描或手动输入此码。",
         "select": "请选择",
         "set_tfa": "设置两步验证方法",
-        "start_u2f_validation": "开始认证",
+        "start_webauthn_validation": "开始认证",
         "tfa": "两步验证(2FA)",
         "totp": "TOTP认证 (Google Authenticator、Authy等)",
-        "u2f": "U2F认证",
-        "waiting_usb_auth": "<i>等待USB设备...</i><br><br>现在请触碰你的U2F USB设备上的按钮。",
-        "waiting_usb_register": "<i>等待USB设备...</i><br><br>请在上方输入你的密码并请触碰你的U2F USB设备上的按钮以确认注册U2F设备。",
+        "webauthn": "WebAuthn认证",
+        "waiting_usb_auth": "<i>等待USB设备...</i><br><br>现在请触碰你的WebAuthn USB设备上的按钮。",
+        "waiting_usb_register": "<i>等待USB设备...</i><br><br>请在上方输入你的密码并请触碰你的WebAuthn USB设备上的按钮以确认注册WebAuthn设备。",
         "yubi_otp": "Yubico OTP认证"
     },
     "user": {

+ 1 - 1
data/web/templates/admin/tab-config-admins.twig

@@ -40,7 +40,7 @@
           <div class="col-sm-9 col-xs-7">
             <select data-style="btn btn-sm dropdown-toggle bs-placeholder btn-default" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
               <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
-              <option value="u2f">{{ lang.tfa.u2f }}</option>
+              <option value="webauthn">{{ lang.tfa.webauthn }}</option>
               <option value="totp">{{ lang.tfa.totp }}</option>
               <option value="none">{{ lang.tfa.none }}</option>
             </select>

+ 98 - 62
data/web/templates/base.twig

@@ -160,7 +160,7 @@ function recursiveBase64StrToArrayBuffer(obj) {
       }
     }
   }
-  }
+}
   $(window).load(function() {
     $(".overlay").hide();
   });
@@ -181,32 +181,52 @@ function recursiveBase64StrToArrayBuffer(obj) {
       backdrop: 'static',
       keyboard: false
     });
-    $('#u2f_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_u2f + '</p>');
+    $('#webauthn_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_u2f + '</p>');
     $('#ConfirmTFAModal').on('shown.bs.modal', function(){
       $(this).find('input[name=token]').focus();
-      // If U2F
-      if(document.getElementById("u2f_auth_data") !== null) {
-        $.ajax({
-          type: "GET",
-          cache: false,
-          dataType: 'script',
-          url: "/api/v1/get/u2f-authentication/{{ pending_mailcow_cc_username|url_encode(true)|default('null') }}",
-          complete: function(data){
-            $('#u2f_status_auth').html(lang_tfa.waiting_usb_auth);
-            data;
-            setTimeout(function() {
-              console.log("Ready to authenticate");
-              u2f.sign(appId, challenge, registeredKeys, function(data) {
-                var form = document.getElementById('u2f_auth_form');
-                var auth = document.getElementById('u2f_auth_data');
-                console.log("Authenticate callback", data);
-                auth.value = JSON.stringify(data);
-                form.submit();
-              });
-            }, 1000);
-          }
+      // If WebAuthn
+      if(document.getElementById("webauthn_auth_data") !== null) {
+        // Check Browser support
+        if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
+            window.alert('Browser not supported for WebAuthn.');
+            return;
+        }
+
+        window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
+            return response.json();
+        }).then(json => {
+            console.log(json);
+            if (json.success === false) throw new Error();
+      
+            recursiveBase64StrToArrayBuffer(json);
+            return json;
+        }).then(getCredentialArgs => {
+            console.log(getCredentialArgs);
+            return navigator.credentials.get(getCredentialArgs);
+        }).then(cred => {
+            console.log(cred);
+            return {
+                id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
+                clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+                authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
+                signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
+            };
+        }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
+            console.log(AuthenticatorAttestationResponse);
+
+            var form = document.getElementById('webauthn_auth_form');
+            var auth = document.getElementById('webauthn_auth_data');
+            console.log("Authenticate callback", AuthenticatorAttestationResponse);
+            auth.value = AuthenticatorAttestationResponse;
+            form.submit();
+        }).catch(function(err) {
+            if (typeof err.message === 'undefined') {
+                mailcow_alert_box(lang_fido2.fido2_validation_failed, "danger");
+            } else {
+                mailcow_alert_box(lang_fido2.fido2_validation_failed + ":<br><i>" + err.message + "</i>", "danger");
+            }
         });
-      }
+      } 
     });
     $('#ConfirmTFAModal').on('hidden.bs.modal', function(){
       $.ajax({
@@ -327,46 +347,62 @@ function recursiveBase64StrToArrayBuffer(obj) {
       });
       $("option:selected").prop("selected", false);
     }
-    if ($(this).val() == "u2f") {
-      $('#U2FModal').modal('show');
-      $("option:selected").prop("selected", false);
-      $("#start_u2f_register").click(function(){
-        $('#u2f_return_code').html('');
-        $('#u2f_return_code').hide();
-        $('#u2f_status_reg').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_u2f + '</p>');
-        $.ajax({
-          type: "GET",
-          cache: false,
-          dataType: 'script',
-          url: "/api/v1/get/u2f-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}",
-          complete: function(data){
-            data;
-            setTimeout(function() {
-              console.log("Ready to register");
-              $('#u2f_status_reg').html(lang_tfa.waiting_usb_register);
-              u2f.register(appId, registerRequests, registeredKeys, function(deviceResponse) {
-                var form  = document.getElementById('u2f_reg_form');
-                var reg   = document.getElementById('u2f_register_data');
-                console.log("Register callback: ", data);
-                if (deviceResponse.errorCode && deviceResponse.errorCode != 0) {
-                  var u2f_return_code = document.getElementById('u2f_return_code');
-                  u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
-                  if (deviceResponse.errorCode == "4") {
-                    deviceResponse.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle";
-                  }
-                  else if (deviceResponse.errorCode == "5") {
-                    deviceResponse.errorCode = "5 - Timeout reached before request could be satisfied.";
-                  }
-                  u2f_return_code.innerHTML = lang_tfa.error_code + ': ' + deviceResponse.errorCode + ' ' + lang_tfa.reload_retry;
-                  return;
+    if ($(this).val() == "webauthn") {
+        // check if Browser is supported
+        if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
+            window.alert('Browser not supported.');
+            return;
+        }
+
+        // show modal
+        $('#WebAuthnModal').modal('show');
+        $("option:selected").prop("selected", false);
+
+        $("#start_webauthn_register").click(() => {
+            var key_id = document.getElementsByName('key_id')[1].value;
+
+            // fetch WebAuthn CreateArgs
+            window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => {
+                return response.json();
+            }).then(json => {
+                console.log(json);
+
+                if (json.success === false) throw new Error(json.msg);
+
+                recursiveBase64StrToArrayBuffer(json);
+
+                return json;
+            }).then(createCredentialArgs => {
+                return navigator.credentials.create(createCredentialArgs);
+            }).then(cred => {
+                return {
+                    clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+                    attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
+                    key_id: key_id,
+                    tfa_method: "webauthn"
+                };
+            }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
+                console.log(AuthenticatorAttestationResponse);
+
+                return window.fetch("/api/v1/add/webauthn-tfa-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
+            }).then(response => {
+                return response.json();
+            }).then(json => {
+                console.log(json);
+
+                if (json.success) {
+                    console.log("success");
+                    window.location.reload();
+                } else {
+                    throw new Error(json.msg);
                 }
-                reg.value = JSON.stringify(deviceResponse);
-                form.submit();
-              });
-            }, 1000);
-          }
+            }).catch(function(err) {
+                console.log(err);
+                var webauthn_return_code = document.getElementById('webauthn_return_code');
+                webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
+                webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
+            });
         });
-      });
     }
     if ($(this).val() == "none") {
       $('#DisableTFAModal').modal('show');

+ 47 - 27
data/web/templates/modals/footer.twig

@@ -37,15 +37,15 @@
   </div>
 </div>
 
-<div class="modal fade" id="U2FModal" tabindex="-1" role="dialog" aria-labelledby="U2FModalLabel">
+<div class="modal fade" id="WebAuthnModal" tabindex="-1" role="dialog" aria-labelledby="WebAuthnModalLabel">
   <div class="modal-dialog" role="document">
     <div class="modal-content">
       <div class="modal-header">
         <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
-        <h3 class="modal-title">{{ lang.tfa.u2f }}</h3>
+        <h3 class="modal-title">{{ lang.tfa.webauthn }}</h3>
       </div>
       <div class="modal-body">
-        <form role="form" method="post" id="u2f_reg_form">
+        <form role="form" method="post" id="webauthn_reg_form">
           <div class="form-group">
             <input type="text" class="form-control" name="key_id" placeholder="{{ lang.tfa.key_id }}" autocomplete="off" required>
           </div>
@@ -54,18 +54,18 @@
           </div>
           <hr>
           <center>
-            <div style="cursor:pointer" id="start_u2f_register">
+            <div style="cursor:pointer" id="start_webauthn_register">
               <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
                 <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
               </svg>
-              <p>{{ lang.tfa.start_u2f_validation }}</p>
+              <p>{{ lang.tfa.start_webauthn_validation }}</p>
               <hr>
             </div>
           </center>
-          <p id="u2f_status_reg"></p>
-          <div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
-          <input type="hidden" name="token" id="u2f_register_data"/>
-          <input type="hidden" name="tfa_method" value="u2f">
+          <p id="webauthn_status_reg"></p>
+          <div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
+          <input type="hidden" name="token" id="webauthn_register_data"/>
+          <input type="hidden" name="tfa_method" value="webauthn">
           <input type="hidden" name="set_tfa"/><br/>
         </form>
       </div>
@@ -154,24 +154,6 @@
           <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-sm btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
         </form>
         {% endif %}
-        {% if pending_tfa_method == 'u2f' %}
-        <form role="form" method="post" id="u2f_auth_form">
-          <center>
-            <div id="start_u2f_confirmation">
-              <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
-                <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
-              </svg>
-              <p>{{ lang.tfa.start_u2f_validation }}</p>
-              <hr>
-            </div>
-          </center>
-          <p id="u2f_status_auth"></p>
-          <div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
-          <input type="hidden" name="token" id="u2f_auth_data"/>
-          <input type="hidden" name="tfa_method" value="u2f">
-          <input type="hidden" name="verify_tfa_login"/><br/>
-        </form>
-        {% endif %}
         {% if pending_tfa_method == 'totp' %}
         <form role="form" method="post">
           <div class="form-group">
@@ -187,6 +169,44 @@
         {% if pending_tfa_method == 'hotp' %}
         <div class="empty"></div>
         {% endif %}
+
+        {% if pending_tfa_method == 'webauthn' %}
+        <form role="form" method="post" id="webauthn_auth_form">
+          <center>
+            <div id="start_webauthn_confirmation">
+              <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
+                <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
+              </svg>
+              <p>{{ lang.tfa.start_webauthn_validation }}</p>
+              <hr>
+            </div>
+          </center>
+          <p id="webauthn_status_auth"></p>
+          <div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
+          <input type="hidden" name="token" id="webauthn_auth_data"/>
+          <input type="hidden" name="tfa_method" value="webauthn">
+          <input type="hidden" name="verify_tfa_login"/><br/>
+        </form>
+        {% endif %}
+        {# leave this here to support older keys that used u2f for registration #}
+        {% if pending_tfa_method == 'u2f' %}
+        <form role="form" method="post" id="webauthn_auth_form">
+          <center>
+            <div id="start_webauthn_confirmation">
+              <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
+                <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
+              </svg>
+              <p>{{ lang.tfa.start_webauthn_validation }}</p>
+              <hr>
+            </div>
+          </center>
+          <p id="webauthn_status_auth"></p>
+          <div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
+          <input type="hidden" name="token" id="webauthn_auth_data"/>
+          <input type="hidden" name="tfa_method" value="webauthn">
+          <input type="hidden" name="verify_tfa_login"/><br/>
+        </form>
+        {% endif %}
       </div>
     </div>
   </div>