Merge branch 'staging' into master
This commit is contained in:
@@ -17,7 +17,8 @@ if (is_array($alertbox_log_parser)) {
|
||||
}
|
||||
$alert = array_filter(array_unique($alerts));
|
||||
foreach($alert as $alert_type => $alert_msg) {
|
||||
$alerts[$alert_type] = implode('<hr class="alert-hr">', $alert_msg);
|
||||
// html breaks from mysql alerts, replace ` with '
|
||||
$alerts[$alert_type] = implode('<hr class="alert-hr">', str_replace("`", "'", $alert_msg));
|
||||
}
|
||||
unset($_SESSION['return']);
|
||||
}
|
||||
|
@@ -1140,7 +1140,6 @@ function is_valid_domain_name($domain_name) {
|
||||
function set_tfa($_data) {
|
||||
global $pdo;
|
||||
global $yubi;
|
||||
global $u2f;
|
||||
global $tfa;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
|
||||
@@ -1183,6 +1182,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,31 +1241,6 @@ function set_tfa($_data) {
|
||||
'msg' => array('object_modified', htmlspecialchars($username))
|
||||
);
|
||||
break;
|
||||
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),
|
||||
'msg' => array('object_modified', $username)
|
||||
);
|
||||
$_SESSION['regReq'] = null;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => array('u2f_verification_failed', $e->getMessage())
|
||||
);
|
||||
$_SESSION['regReq'] = null;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "totp":
|
||||
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
|
||||
if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
|
||||
@@ -1286,6 +1262,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));
|
||||
@@ -1360,8 +1359,8 @@ function fido2($_data) {
|
||||
if (!isset($_data['cid']) || empty($_data['cid'])) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE TO_BASE64(`credentialId`) = :cid");
|
||||
$stmt->execute(array(':cid' => $_data['cid']));
|
||||
$stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE `credentialId` = :cid");
|
||||
$stmt->execute(array(':cid' => base64_decode($_data['cid'])));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
|
||||
return false;
|
||||
@@ -1516,6 +1515,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 +1534,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 +1546,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 +1573,192 @@ 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":
|
||||
// delete old keys that used u2f
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username");
|
||||
$stmt->execute(array(':authmech' => 'u2f', ':username' => $username));
|
||||
|
||||
return true;
|
||||
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;
|
||||
'msg' => 'unknown_tfa_method'
|
||||
);
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
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,12 +2020,7 @@ function rspamd_ui($action, $data = null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
function get_u2f_registrations($username) {
|
||||
global $pdo;
|
||||
$sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");
|
||||
$sel->execute(array($username));
|
||||
return $sel->fetchAll(PDO::FETCH_OBJ);
|
||||
}
|
||||
|
||||
function get_logs($application, $lines = false) {
|
||||
if ($lines === false) {
|
||||
$lines = $GLOBALS['LOG_LINES'] - 1;
|
||||
|
@@ -3,7 +3,7 @@ function init_db_schema() {
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "31102021_0620";
|
||||
$db_version = "18012022_1020";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -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",
|
||||
@@ -1189,6 +1189,9 @@ function init_db_schema() {
|
||||
|
||||
// Mitigate imapsync pipemess issue
|
||||
$pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%';");
|
||||
|
||||
// Migrate webauthn tfa
|
||||
$stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
|
||||
|
||||
// Inject admin if not exists
|
||||
$stmt = $pdo->query("SELECT NULL FROM `admin`");
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
// -----------------------
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
@@ -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,5 +1,5 @@
|
||||
<?php
|
||||
namespace WebAuthn;
|
||||
namespace lbuchs\WebAuthn;
|
||||
|
||||
/**
|
||||
* @author Lukas Buchs
|
||||
|
@@ -54,6 +54,7 @@ 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);
|
||||
@@ -61,17 +62,8 @@ $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
|
||||
// FIDO2
|
||||
$formats = $GLOBALS['FIDO2_FORMATS'];
|
||||
$WebAuthn = new \WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['SERVER_NAME'], $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');
|
||||
// only include root ca's when needed
|
||||
if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates');
|
||||
|
||||
// Redis
|
||||
$redis = new Redis();
|
||||
|
@@ -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"]);
|
||||
}
|
||||
|
@@ -175,6 +175,9 @@ $MAILBOX_DEFAULT_ATTRIBUTES['pop3_access'] = true;
|
||||
// Mailbox has SMTP access by default
|
||||
$MAILBOX_DEFAULT_ATTRIBUTES['smtp_access'] = true;
|
||||
|
||||
// Mailbox has sieve access by default
|
||||
$MAILBOX_DEFAULT_ATTRIBUTES['sieve_access'] = true;
|
||||
|
||||
// Mailbox receives notifications about...
|
||||
// "add_header" - mail that was put into the Junk folder
|
||||
// "reject" - mail that was rejected
|
||||
@@ -192,11 +195,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(
|
||||
|
Reference in New Issue
Block a user