[Web] Feature (beta): Add WebAuthn support for administrators and domain administrators
parent
58cce74bc9
commit
c150ac7b37
|
@ -5,6 +5,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
$tfa_data = get_tfa();
|
$tfa_data = get_tfa();
|
||||||
|
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||||
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
|
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
|
||||||
$_SESSION['gal'] = json_decode($license_cache, true);
|
$_SESSION['gal'] = json_decode($license_cache, true);
|
||||||
}
|
}
|
||||||
|
@ -61,34 +62,39 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
|
||||||
<a class="btn btn-sm btn-success" data-id="add_admin" data-toggle="modal" data-target="#addAdminModal" href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add_admin'];?></a>
|
<a class="btn btn-sm btn-success" data-id="add_admin" data-toggle="modal" data-target="#addAdminModal" href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add_admin'];?></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<? // TFA ?>
|
||||||
<legend style="margin-top:20px">
|
<legend style="margin-top:20px">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="margin-bottom: -5px;">
|
<?=$lang['tfa']['tfa'];?>
|
||||||
<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"/>
|
</legend>
|
||||||
</svg> <?=$lang['tfa']['tfa'];?></legend>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?>:</div>
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?>:</div>
|
||||||
<div class="col-sm-9 col-xs-7">
|
<div class="col-sm-9 col-xs-7">
|
||||||
<p id="tfa_pretty"><?=$tfa_data['pretty'];?></p>
|
<p id="tfa_pretty"><?=$tfa_data['pretty'];?></p>
|
||||||
<div id="tfa_additional">
|
<div id="tfa_additional">
|
||||||
<?php if (!empty($tfa_data['additional'])):
|
<?php
|
||||||
foreach ($tfa_data['additional'] as $key_info): ?>
|
if (!empty($tfa_data['additional'])) {
|
||||||
|
foreach ($tfa_data['additional'] as $key_info) {
|
||||||
|
?>
|
||||||
<form style="display:inline;" method="post">
|
<form style="display:inline;" method="post">
|
||||||
<input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
|
<input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
|
||||||
<div style="padding:4px;margin:4px" class="label label-<?=($_SESSION['tfa_id'] == $key_info['id']) ? 'success' : 'default'; ?>">
|
<div style="padding:4px;margin:4px" class="label label-keys label-<?=($_SESSION['tfa_id'] == $key_info['id']) ? 'success' : 'default'; ?>">
|
||||||
<?=$key_info['key_id'];?>
|
<?=$key_info['key_id'];?>
|
||||||
<a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=strtolower($lang['admin']['remove']);?>]</a>
|
<a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=strtolower($lang['admin']['remove']);?>]</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?php endforeach;
|
<?php
|
||||||
endif;?>
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
|
||||||
<div class="col-sm-9 col-xs-7">
|
<div class="col-sm-9 col-xs-7">
|
||||||
<select data-width="fit" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
|
<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="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
|
||||||
<option value="u2f"><?=$lang['tfa']['u2f'];?></option>
|
<option value="u2f"><?=$lang['tfa']['u2f'];?></option>
|
||||||
<option value="totp"><?=$lang['tfa']['totp'];?></option>
|
<option value="totp"><?=$lang['tfa']['totp'];?></option>
|
||||||
|
@ -97,10 +103,61 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<legend style="cursor:pointer;" data-target="#license" class="arrow-toggle" unselectable="on" data-toggle="collapse">
|
<? // FIDO2 ?>
|
||||||
<span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-up"></span> <?=$lang['admin']['guid_and_license'];?>
|
<legend style="margin-top:20px">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="margin-bottom: -5px;">
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
<?=$lang['fido2']['fido2_auth'];?></legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['fido2']['known_ids'];?>:</div>
|
||||||
|
<div class="col-sm-9 col-xs-7">
|
||||||
|
<div id="tfa_additional">
|
||||||
|
<?php
|
||||||
|
if (!empty($fido2_data)) {
|
||||||
|
foreach ($fido2_data as $key_info) {
|
||||||
|
?>
|
||||||
|
<form style="display:inline;" method="post">
|
||||||
|
<input type="hidden" name="unset_fido2_key" value="<?=$key_info['subject'];?>" />
|
||||||
|
<p><div data-toggle="tooltip" data-placement="top" title="<?=$key_info['subject'];?>" class="label label-keys label-<?=($_SESSION['fido2_subject'] == $key_info['subject']) ? 'success' : 'default'; ?>">
|
||||||
|
<?=(!empty($key_info['fn']))?$key_info['fn']:$key_info['subject'];?>
|
||||||
|
<a href="#" class="key-action" onClick='return confirm("<?=$lang['admin']['ays'];?>")?$(this).closest("form").submit():"";'>
|
||||||
|
[<?=strtolower($lang['admin']['remove']);?>]
|
||||||
|
</a>
|
||||||
|
<a href="#" class="key-action" data-subject="<?=base64_encode($key_info['subject']);?>" data-toggle="modal" data-target="#fido2ChangeFn">
|
||||||
|
[<?=strtolower($lang['fido2']['rename']);?>]
|
||||||
|
</a>
|
||||||
|
</div></p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo "-";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
|
<button class="btn btn-sm btn-primary" id="register-fido2"><?=$lang['fido2']['set_fido2'];?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row" id="status-fido2">
|
||||||
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['fido2']['register_status'];?>:</div>
|
||||||
|
<div class="col-sm-9 col-xs-7">
|
||||||
|
<div id="fido2-alerts">-</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<legend style="cursor:pointer;margin-top:40px" data-target="#license" class="arrow-toggle" unselectable="on" data-toggle="collapse">
|
||||||
|
<span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['guid_and_license'];?>
|
||||||
</legend>
|
</legend>
|
||||||
<div id="license" class="collapse in">
|
<div id="license" class="collapse">
|
||||||
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
|
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-sm-3" for="guid"><?=$lang['admin']['guid'];?>:</label>
|
<label class="control-label col-sm-3" for="guid"><?=$lang['admin']['guid'];?>:</label>
|
||||||
|
@ -466,7 +523,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
|
||||||
<div class="row collapse in dkim_key_missing">
|
<div class="row collapse in dkim_key_missing">
|
||||||
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
|
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
|
<p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8"><pre>-</pre></div>
|
<div class="col-md-8"><pre>-</pre></div>
|
||||||
<hr class="visible-xs visible-sm">
|
<hr class="visible-xs visible-sm">
|
||||||
|
@ -500,7 +557,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
|
||||||
<div class="row collapse in dkim_key_missing">
|
<div class="row collapse in dkim_key_missing">
|
||||||
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" disabled /></div>
|
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" disabled /></div>
|
||||||
<div class="col-md-2 col-md-offset-1">
|
<div class="col-md-2 col-md-offset-1">
|
||||||
<p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
|
<p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8"><pre>-</pre></div>
|
<div class="col-md-8"><pre>-</pre></div>
|
||||||
<hr class="visible-xs visible-sm">
|
<hr class="visible-xs visible-sm">
|
||||||
|
|
|
@ -77,3 +77,12 @@ table tbody tr td input[type="checkbox"] {
|
||||||
font-family: Consolas,monaco,monospace;
|
font-family: Consolas,monaco,monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
.label-keys {
|
||||||
|
font-size:100%;
|
||||||
|
margin: 0px !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
.key-action {
|
||||||
|
font-weight:bold;
|
||||||
|
color:white !important;
|
||||||
|
}
|
|
@ -58,3 +58,12 @@ table tbody tr td input[type="checkbox"] {
|
||||||
-webkit-transform:rotateX(180deg);
|
-webkit-transform:rotateX(180deg);
|
||||||
transform:rotateX(180deg);
|
transform:rotateX(180deg);
|
||||||
}
|
}
|
||||||
|
.label-keys {
|
||||||
|
font-size:100%;
|
||||||
|
margin: 0px !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
.key-action {
|
||||||
|
font-weight:bold;
|
||||||
|
color:white !important;
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
session_start();
|
|
||||||
header("Content-Type: application/json");
|
header("Content-Type: application/json");
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
session_start();
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_GET['token']) && ctype_alnum($_GET['token'])) {
|
if (isset($_GET['token']) && ctype_alnum($_GET['token'])) {
|
||||||
echo $tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $_GET['token']);
|
echo $tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $_GET['token']);
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
session_start();
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
session_start();
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||||
|
|
|
@ -15,9 +15,11 @@ if(!file_exists($JSPath)) {
|
||||||
$lang_footer = json_encode($lang['footer']);
|
$lang_footer = json_encode($lang['footer']);
|
||||||
$lang_acl = json_encode($lang['acl']);
|
$lang_acl = json_encode($lang['acl']);
|
||||||
$lang_tfa = json_encode($lang['tfa']);
|
$lang_tfa = json_encode($lang['tfa']);
|
||||||
|
$lang_fido2 = json_encode($lang['fido2']);
|
||||||
echo "var lang_footer = ". $lang_footer . ";\n";
|
echo "var lang_footer = ". $lang_footer . ";\n";
|
||||||
echo "var lang_acl = ". $lang_acl . ";\n";
|
echo "var lang_acl = ". $lang_acl . ";\n";
|
||||||
echo "var lang_tfa = ". $lang_tfa . ";\n";
|
echo "var lang_tfa = ". $lang_tfa . ";\n";
|
||||||
|
echo "var lang_fido2 = ". $lang_fido2 . ";\n";
|
||||||
echo "var docker_timeout = ". $DOCKER_TIMEOUT * 1000 . ";\n";
|
echo "var docker_timeout = ". $DOCKER_TIMEOUT * 1000 . ";\n";
|
||||||
?>
|
?>
|
||||||
$(window).scroll(function() {
|
$(window).scroll(function() {
|
||||||
|
@ -28,6 +30,39 @@ function setLang(sel) {
|
||||||
$.post( "<?= $_SERVER['REQUEST_URI']; ?>", {lang: sel} );
|
$.post( "<?= $_SERVER['REQUEST_URI']; ?>", {lang: sel} );
|
||||||
window.location.href = window.location.pathname + window.location.search;
|
window.location.href = window.location.pathname + window.location.search;
|
||||||
}
|
}
|
||||||
|
// FIDO2 functions
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
let binary = '';
|
||||||
|
let bytes = new Uint8Array(buffer);
|
||||||
|
let len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode( bytes[ i ] );
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
function recursiveBase64StrToArrayBuffer(obj) {
|
||||||
|
let prefix = '=?BINARY?B?';
|
||||||
|
let suffix = '?=';
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
for (let key in obj) {
|
||||||
|
if (typeof obj[key] === 'string') {
|
||||||
|
let str = obj[key];
|
||||||
|
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||||||
|
str = str.substring(prefix.length, str.length - suffix.length);
|
||||||
|
let binary_string = window.atob(str);
|
||||||
|
let len = binary_string.length;
|
||||||
|
let bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
obj[key] = bytes.buffer;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recursiveBase64StrToArrayBuffer(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$(window).load(function() {
|
$(window).load(function() {
|
||||||
$(".overlay").hide();
|
$(".overlay").hide();
|
||||||
});
|
});
|
||||||
|
@ -97,8 +132,81 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
// Validate FIDO2
|
||||||
// Set TFA modals
|
$("#fido2-login").click(function(){
|
||||||
|
$('#fido2-alerts').html();
|
||||||
|
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
|
||||||
|
window.alert('Browser not supported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.fetch("/api/v1/get/fido2-get-args", {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
if (json.success === false) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
recursiveBase64StrToArrayBuffer(json);
|
||||||
|
return json;
|
||||||
|
}).then(function(getCredentialArgs) {
|
||||||
|
return navigator.credentials.get(getCredentialArgs);
|
||||||
|
}).then(function(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) {
|
||||||
|
return window.fetch("/api/v1/process/fido2-args", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
if (json.success) {
|
||||||
|
window.location = window.location.href.split("#")[0];
|
||||||
|
} else {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
mailcow_alert_box(lang_fido2.fido2_validation_failed, "danger");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Set TFA/FIDO2
|
||||||
|
$("#register-fido2").click(function(){
|
||||||
|
$("option:selected").prop("selected", false);
|
||||||
|
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
|
||||||
|
window.alert('Browser not supported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.fetch("/api/v1/get/fido2-registration/<?= (isset($_SESSION['mailcow_cc_username'])) ? rawurlencode($_SESSION['mailcow_cc_username']) : null; ?>", {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
if (json.success === false) {
|
||||||
|
throw new Error(json.msg);
|
||||||
|
}
|
||||||
|
recursiveBase64StrToArrayBuffer(json);
|
||||||
|
return json;
|
||||||
|
}).then(function(createCredentialArgs) {
|
||||||
|
console.log(createCredentialArgs);
|
||||||
|
return navigator.credentials.create(createCredentialArgs);
|
||||||
|
}).then(function(cred) {
|
||||||
|
return {
|
||||||
|
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||||
|
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
|
||||||
|
};
|
||||||
|
}).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
|
||||||
|
return window.fetch("/api/v1/add/fido2-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function(json) {
|
||||||
|
if (json.success) {
|
||||||
|
window.location = window.location.href.split("#")[0];
|
||||||
|
} else {
|
||||||
|
throw new Error(json.msg);
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
$('#fido2-alerts').html('<span class="text-danger"><b>' + err.message + '</b></span>');
|
||||||
|
});
|
||||||
|
});
|
||||||
$('#selectTFA').change(function () {
|
$('#selectTFA').change(function () {
|
||||||
if ($(this).val() == "yubi_otp") {
|
if ($(this).val() == "yubi_otp") {
|
||||||
$('#YubiOTPModal').modal('show');
|
$('#YubiOTPModal').modal('show');
|
||||||
|
|
|
@ -924,20 +924,10 @@ function set_tfa($_data) {
|
||||||
case "totp":
|
case "totp":
|
||||||
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
|
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
|
||||||
if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
|
if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
|
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
|
||||||
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
|
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
|
||||||
'msg' => array('mysql_error', $e)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
|
@ -953,18 +943,142 @@ function set_tfa($_data) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "none":
|
case "none":
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
|
'msg' => array('object_modified', htmlspecialchars($username))
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
}
|
||||||
|
function fido2($_data) {
|
||||||
|
global $pdo;
|
||||||
|
$_data_log = $_data;
|
||||||
|
// Not logging registration data, only actions
|
||||||
|
// Silent errors for "get" requests
|
||||||
|
switch ($_data["action"]) {
|
||||||
|
case "register":
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
|
||||||
|
$_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data["action"]),
|
||||||
'msg' => array('mysql_error', $e)
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute(array(
|
||||||
|
$username,
|
||||||
|
$_data['registration']->rpId,
|
||||||
|
$_data['registration']->credentialPublicKey,
|
||||||
|
$_data['registration']->certificateChain,
|
||||||
|
$_data['registration']->certificate,
|
||||||
|
$_data['registration']->certificateIssuer,
|
||||||
|
$_data['registration']->certificateSubject,
|
||||||
|
$_data['registration']->signatureCounter,
|
||||||
|
$_data['registration']->AAGUID,
|
||||||
|
$_data['registration']->credentialId)
|
||||||
|
);
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_data["action"]),
|
||||||
|
'msg' => array('object_modified', $username)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "get_user_cids":
|
||||||
|
// Used to exclude existing CredentialIds while registering
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
|
||||||
|
$_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
while($row = array_shift($rows)) {
|
||||||
|
$cids[] = $row['credentialId'];
|
||||||
|
}
|
||||||
|
return $cids;
|
||||||
|
break;
|
||||||
|
case "get_all_cids":
|
||||||
|
// Only needed when using fido2 with username
|
||||||
|
$stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
while($row = array_shift($rows)) {
|
||||||
|
$cids[] = $row['credentialId'];
|
||||||
|
}
|
||||||
|
return $cids;
|
||||||
|
break;
|
||||||
|
case "get_pub_key":
|
||||||
|
if (!isset($_data['cid']) || empty($_data['cid'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey` FROM `fido2` WHERE TO_BASE64(`credentialId`) = :cid");
|
||||||
|
$stmt->execute(array(':cid' => $_data['cid']));
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$data['pub_key'] = $row['credentialPublicKey'];
|
||||||
|
$data['username'] = $row['username'];
|
||||||
|
$data['key_id'] = $row['certificateSubject'];
|
||||||
|
return $data;
|
||||||
|
break;
|
||||||
|
case "get_friendly_names":
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
|
||||||
|
$_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("SELECT `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
while($row = array_shift($rows)) {
|
||||||
|
$fns[] = array("subject" => $row['certificateSubject'], "fn" => $row['friendlyName']);
|
||||||
|
}
|
||||||
|
return $fns;
|
||||||
|
break;
|
||||||
|
case "unset_fido2_key":
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
|
||||||
|
$_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_data["action"]),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND `certificateSubject` = :certificateSubject");
|
||||||
|
$stmt->execute(array(':username' => $username, ':certificateSubject' => $_data['post_data']['unset_fido2_key']));
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
|
'msg' => array('object_modified', htmlspecialchars($username))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "edit_fn":
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
|
||||||
|
$_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_data["action"]),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE `certificateSubject` = :certificateSubject AND `username` = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':username' => $username,
|
||||||
|
':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
|
||||||
|
':certificateSubject' => base64_decode($_data['fido2_attrs']['fido2_subject'])
|
||||||
|
));
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
|
|
|
@ -3,7 +3,7 @@ function init_db_schema() {
|
||||||
try {
|
try {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
|
|
||||||
$db_version = "06112020_1010";
|
$db_version = "15112020_1110";
|
||||||
|
|
||||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
@ -84,6 +84,31 @@ function init_db_schema() {
|
||||||
),
|
),
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
),
|
),
|
||||||
|
"fido2" => array(
|
||||||
|
"cols" => array(
|
||||||
|
"username" => "VARCHAR(255) NOT NULL",
|
||||||
|
"friendlyName" => "VARCHAR(255)",
|
||||||
|
"rpId" => "VARCHAR(255) NOT NULL",
|
||||||
|
"credentialPublicKey" => "TEXT NOT NULL",
|
||||||
|
"certificateChain" => "TEXT",
|
||||||
|
// Can be null for format "none"
|
||||||
|
"certificate" => "TEXT",
|
||||||
|
"certificateIssuer" => "VARCHAR(255)",
|
||||||
|
"certificateSubject" => "VARCHAR(255)",
|
||||||
|
"signatureCounter" => "INT",
|
||||||
|
"AAGUID" => "BLOB",
|
||||||
|
"credentialId" => "BLOB NOT NULL",
|
||||||
|
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||||
|
"modified" => "DATETIME ON UPDATE NOW(0)",
|
||||||
|
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||||
|
),
|
||||||
|
"keys" => array(
|
||||||
|
"unique" => array(
|
||||||
|
"fido2_username_CID" => array("username", "certificateSubject")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
|
),
|
||||||
"_sogo_static_view" => array(
|
"_sogo_static_view" => array(
|
||||||
"cols" => array(
|
"cols" => array(
|
||||||
"c_uid" => "VARCHAR(255) NOT NULL",
|
"c_uid" => "VARCHAR(255) NOT NULL",
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\CBOR\CborDecoder;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class AttestationObject {
|
||||||
|
private $_authenticatorData;
|
||||||
|
private $_attestationFormat;
|
||||||
|
|
||||||
|
public function __construct($binary , $allowedFormats) {
|
||||||
|
$enc = CborDecoder::decode($binary);
|
||||||
|
// validation
|
||||||
|
if (!\is_array($enc) || !\array_key_exists('fmt', $enc) || !is_string($enc['fmt'])) {
|
||||||
|
throw new WebAuthnException('invalid attestation format', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('attStmt', $enc) || !\is_array($enc['attStmt'])) {
|
||||||
|
throw new WebAuthnException('invalid attestation format (attStmt not available)', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('authData', $enc) || !\is_object($enc['authData']) || !($enc['authData'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid attestation format (authData not available)', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString());
|
||||||
|
|
||||||
|
// Format ok?
|
||||||
|
if (!in_array($enc['fmt'], $allowedFormats)) {
|
||||||
|
throw new WebAuthnException('invalid atttestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($enc['fmt']) {
|
||||||
|
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 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break;
|
||||||
|
case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break;
|
||||||
|
case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break;
|
||||||
|
case 'tpm': $this->_attestationFormat = new Format\Tpm($enc, $this->_authenticatorData); break;
|
||||||
|
default: throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the attestation public key in PEM format
|
||||||
|
* @return AuthenticatorData
|
||||||
|
*/
|
||||||
|
public function getAuthenticatorData() {
|
||||||
|
return $this->_authenticatorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the certificate chain as PEM
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificateChain() {
|
||||||
|
return $this->_attestationFormat->getCertificateChain();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the certificate issuer as string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificateIssuer() {
|
||||||
|
$pem = $this->getCertificatePem();
|
||||||
|
$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 ($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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the certificate subject as string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificateSubject() {
|
||||||
|
$pem = $this->getCertificatePem();
|
||||||
|
$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 ($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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_attestationFormat->getCertificatePem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks validity of the signature
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return $this->_attestationFormat->validateAttestation($clientDataHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
return $this->_attestationFormat->validateRootCertificate($rootCas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if the RpId-Hash is valid
|
||||||
|
* @param string$rpIdHash
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validateRpIdHash($rpIdHash) {
|
||||||
|
return $rpIdHash === $this->_authenticatorData->getRpIdHash();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,423 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\CBOR\CborDecoder;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class AuthenticatorData {
|
||||||
|
protected $_binary;
|
||||||
|
protected $_rpIdHash;
|
||||||
|
protected $_flags;
|
||||||
|
protected $_signCount;
|
||||||
|
protected $_attestedCredentialData;
|
||||||
|
protected $_extensionData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Cose encoded keys
|
||||||
|
private static $_COSE_KTY = 1;
|
||||||
|
private static $_COSE_ALG = 3;
|
||||||
|
|
||||||
|
// Cose EC2 ES256 P-256 curve
|
||||||
|
private static $_COSE_CRV = -1;
|
||||||
|
private static $_COSE_X = -2;
|
||||||
|
private static $_COSE_Y = -3;
|
||||||
|
|
||||||
|
// Cose RSA PS256
|
||||||
|
private static $_COSE_N = -1;
|
||||||
|
private static $_COSE_E = -2;
|
||||||
|
|
||||||
|
private static $_EC2_TYPE = 2;
|
||||||
|
private static $_EC2_ES256 = -7;
|
||||||
|
private static $_EC2_P256 = 1;
|
||||||
|
|
||||||
|
private static $_RSA_TYPE = 3;
|
||||||
|
private static $_RSA_RS256 = -257;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsing the authenticatorData binary.
|
||||||
|
* @param string $binary
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function __construct($binary) {
|
||||||
|
if (!\is_string($binary) || \strlen($binary) < 37) {
|
||||||
|
throw new WebAuthnException('Invalid authenticatorData input', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
$this->_binary = $binary;
|
||||||
|
|
||||||
|
// Read infos from binary
|
||||||
|
// https://www.w3.org/TR/webauthn/#sec-authenticator-data
|
||||||
|
|
||||||
|
// RP ID
|
||||||
|
$this->_rpIdHash = \substr($binary, 0, 32);
|
||||||
|
|
||||||
|
// flags (1 byte)
|
||||||
|
$flags = \unpack('Cflags', \substr($binary, 32, 1))['flags'];
|
||||||
|
$this->_flags = $this->_readFlags($flags);
|
||||||
|
|
||||||
|
// signature counter: 32-bit unsigned big-endian integer.
|
||||||
|
$this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount'];
|
||||||
|
|
||||||
|
$offset = 37;
|
||||||
|
// https://www.w3.org/TR/webauthn/#sec-attested-credential-data
|
||||||
|
if ($this->_flags->attestedDataIncluded) {
|
||||||
|
$this->_attestedCredentialData = $this->_readAttestData($binary, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->_flags->extensionDataIncluded) {
|
||||||
|
$this->_readExtensionData(\substr($binary, $offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticator Attestation Globally Unique Identifier, a unique number
|
||||||
|
* that identifies the model of the authenticator (not the specific instance
|
||||||
|
* of the authenticator)
|
||||||
|
* The aaguid may be 0 if the user is using a old u2f device and/or if
|
||||||
|
* the browser is using the fido-u2f format.
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function getAAGUID() {
|
||||||
|
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
return $this->_attestedCredentialData->aaguid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the authenticatorData as binary
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBinary() {
|
||||||
|
return $this->_binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the credentialId
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function getCredentialId() {
|
||||||
|
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
return $this->_attestedCredentialData->credentialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the public key in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPublicKeyPem() {
|
||||||
|
$der = null;
|
||||||
|
switch ($this->_attestedCredentialData->credentialPublicKey->kty) {
|
||||||
|
case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break;
|
||||||
|
case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break;
|
||||||
|
default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pem = '-----BEGIN PUBLIC KEY-----' . "\n";
|
||||||
|
$pem .= \chunk_split(\base64_encode($der), 64, "\n");
|
||||||
|
$pem .= '-----END PUBLIC KEY-----' . "\n";
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the public key in U2F format
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function getPublicKeyU2F() {
|
||||||
|
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
return "\x04" . // ECC uncompressed
|
||||||
|
$this->_attestedCredentialData->credentialPublicKey->x .
|
||||||
|
$this->_attestedCredentialData->credentialPublicKey->y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the SHA256 hash of the relying party id (=hostname)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRpIdHash() {
|
||||||
|
return $this->_rpIdHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the sign counter
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getSignCount() {
|
||||||
|
return $this->_signCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if the user is present
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function getUserPresent() {
|
||||||
|
return $this->_flags->userPresent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if the user is verified
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function getUserVerified() {
|
||||||
|
return $this->_flags->userVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// PRIVATE
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns DER encoded EC2 key
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function _getEc2Der() {
|
||||||
|
return $this->_der_sequence(
|
||||||
|
$this->_der_sequence(
|
||||||
|
$this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey
|
||||||
|
$this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1
|
||||||
|
) .
|
||||||
|
$this->_der_bitString($this->getPublicKeyU2F())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns DER encoded RSA key
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function _getRsaDer() {
|
||||||
|
return $this->_der_sequence(
|
||||||
|
$this->_der_sequence(
|
||||||
|
$this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption
|
||||||
|
$this->_der_nullValue()
|
||||||
|
) .
|
||||||
|
$this->_der_bitString(
|
||||||
|
$this->_der_sequence(
|
||||||
|
$this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) .
|
||||||
|
$this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads the flags from flag byte
|
||||||
|
* @param string $binFlag
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
private function _readFlags($binFlag) {
|
||||||
|
$flags = new \stdClass();
|
||||||
|
|
||||||
|
$flags->bit_0 = !!($binFlag & 1);
|
||||||
|
$flags->bit_1 = !!($binFlag & 2);
|
||||||
|
$flags->bit_2 = !!($binFlag & 4);
|
||||||
|
$flags->bit_3 = !!($binFlag & 8);
|
||||||
|
$flags->bit_4 = !!($binFlag & 16);
|
||||||
|
$flags->bit_5 = !!($binFlag & 32);
|
||||||
|
$flags->bit_6 = !!($binFlag & 64);
|
||||||
|
$flags->bit_7 = !!($binFlag & 128);
|
||||||
|
|
||||||
|
// named flags
|
||||||
|
$flags->userPresent = $flags->bit_0;
|
||||||
|
$flags->userVerified = $flags->bit_2;
|
||||||
|
$flags->attestedDataIncluded = $flags->bit_6;
|
||||||
|
$flags->extensionDataIncluded = $flags->bit_7;
|
||||||
|
return $flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* read attested data
|
||||||
|
* @param string $binary
|
||||||
|
* @param int $endOffset
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readAttestData($binary, &$endOffset) {
|
||||||
|
$attestedCData = new \stdClass();
|
||||||
|
if (\strlen($binary) <= 55) {
|
||||||
|
throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The AAGUID of the authenticator
|
||||||
|
$attestedCData->aaguid = \substr($binary, 37, 16);
|
||||||
|
|
||||||
|
//Byte length L of Credential ID, 16-bit unsigned big-endian integer.
|
||||||
|
$length = \unpack('nlength', \substr($binary, 53, 2))['length'];
|
||||||
|
$attestedCData->credentialId = \substr($binary, 55, $length);
|
||||||
|
|
||||||
|
// set end offset
|
||||||
|
$endOffset = 55 + $length;
|
||||||
|
|
||||||
|
// extract public key
|
||||||
|
$attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset);
|
||||||
|
|
||||||
|
return $attestedCData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads COSE key-encoded elliptic curve public key in EC2 format
|
||||||
|
* @param string $binary
|
||||||
|
* @param int $endOffset
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readCredentialPublicKey($binary, $offset, &$endOffset) {
|
||||||
|
$enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset);
|
||||||
|
|
||||||
|
// COSE key-encoded elliptic curve public key in EC2 format
|
||||||
|
$credPKey = new \stdClass();
|
||||||
|
$credPKey->kty = $enc[self::$_COSE_KTY];
|
||||||
|
$credPKey->alg = $enc[self::$_COSE_ALG];
|
||||||
|
|
||||||
|
switch ($credPKey->alg) {
|
||||||
|
case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break;
|
||||||
|
case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $credPKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extract ES256 informations from cose
|
||||||
|
* @param \stdClass $credPKey
|
||||||
|
* @param \stdClass $enc
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readCredentialPublicKeyES256(&$credPKey, $enc) {
|
||||||
|
$credPKey->crv = $enc[self::$_COSE_CRV];
|
||||||
|
$credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null;
|
||||||
|
$credPKey->y = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null;
|
||||||
|
unset ($enc);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if ($credPKey->kty !== self::$_EC2_TYPE) {
|
||||||
|
throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credPKey->alg !== self::$_EC2_ES256) {
|
||||||
|
throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credPKey->crv !== self::$_EC2_P256) {
|
||||||
|
throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->x) !== 32) {
|
||||||
|
throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->y) !== 32) {
|
||||||
|
throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extract RS256 informations from COSE
|
||||||
|
* @param \stdClass $credPKey
|
||||||
|
* @param \stdClass $enc
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readCredentialPublicKeyRS256(&$credPKey, $enc) {
|
||||||
|
$credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null;
|
||||||
|
$credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null;
|
||||||
|
unset ($enc);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if ($credPKey->kty !== self::$_RSA_TYPE) {
|
||||||
|
throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credPKey->alg !== self::$_RSA_RS256) {
|
||||||
|
throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->n) !== 256) {
|
||||||
|
throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->e) !== 3) {
|
||||||
|
throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads cbor encoded extension data.
|
||||||
|
* @param string $binary
|
||||||
|
* @return array
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readExtensionData($binary) {
|
||||||
|
$ext = CborDecoder::decode($binary);
|
||||||
|
if (!\is_array($ext)) {
|
||||||
|
throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// DER functions
|
||||||
|
// ---------------
|
||||||
|
|
||||||
|
private function _der_length($len) {
|
||||||
|
if ($len < 128) {
|
||||||
|
return \chr($len);
|
||||||
|
}
|
||||||
|
$lenBytes = '';
|
||||||
|
while ($len > 0) {
|
||||||
|
$lenBytes = \chr($len % 256) . $lenBytes;
|
||||||
|
$len = \intdiv($len, 256);
|
||||||
|
}
|
||||||
|
return \chr(0x80 | \strlen($lenBytes)) . $lenBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_sequence($contents) {
|
||||||
|
return "\x30" . $this->_der_length(\strlen($contents)) . $contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_oid($encoded) {
|
||||||
|
return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_bitString($bytes) {
|
||||||
|
return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_nullValue() {
|
||||||
|
return "\x05\x00";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_unsignedInteger($bytes) {
|
||||||
|
$len = \strlen($bytes);
|
||||||
|
|
||||||
|
// Remove leading zero bytes
|
||||||
|
for ($i = 0; $i < ($len - 1); $i++) {
|
||||||
|
if (\ord($bytes[$i]) !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($i !== 0) {
|
||||||
|
$bytes = \substr($bytes, $i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If most significant bit is set, prefix with another zero to prevent it being seen as negative number
|
||||||
|
if ((\ord($bytes[0]) & 0x80) !== 0) {
|
||||||
|
$bytes = "\x00" . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class AndroidKey extends FormatBase {
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check u2f data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
$this->_x5c = $attStmt['x5c'][0]->getBinaryString();
|
||||||
|
|
||||||
|
if (count($attStmt['x5c']) > 1) {
|
||||||
|
for ($i=1; $i<count($attStmt['x5c']); $i++) {
|
||||||
|
$this->_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString();
|
||||||
|
}
|
||||||
|
unset ($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||||
|
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class AndroidSafetyNet extends FormatBase {
|
||||||
|
private $_signature;
|
||||||
|
private $_signedValue;
|
||||||
|
private $_x5c;
|
||||||
|
private $_payload;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) {
|
||||||
|
throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $attStmt['response']->getBinaryString();
|
||||||
|
|
||||||
|
// Response is a JWS [RFC7515] object in Compact Serialization.
|
||||||
|
// JWSs have three segments separated by two period ('.') characters
|
||||||
|
$parts = \explode('.', $response);
|
||||||
|
unset ($response);
|
||||||
|
if (\count($parts) !== 3) {
|
||||||
|
throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = $this->_base64url_decode($parts[0]);
|
||||||
|
$payload = $this->_base64url_decode($parts[1]);
|
||||||
|
$this->_signature = $this->_base64url_decode($parts[2]);
|
||||||
|
$this->_signedValue = $parts[0] . '.' . $parts[1];
|
||||||
|
unset ($parts);
|
||||||
|
|
||||||
|
$header = \json_decode($header);
|
||||||
|
$payload = \json_decode($payload);
|
||||||
|
|
||||||
|
if (!($header instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
if (!($payload instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$header->x5c || !is_array($header->x5c) || count($header->x5c) === 0) {
|
||||||
|
throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// algorithm
|
||||||
|
if (!\in_array($header->alg, array('RS256', 'ES256'))) {
|
||||||
|
throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = \base64_decode($header->x5c[0]);
|
||||||
|
$this->_payload = $payload;
|
||||||
|
|
||||||
|
if (count($header->x5c) > 1) {
|
||||||
|
for ($i=1; $i<count($header->x5c); $i++) {
|
||||||
|
$this->_x5c_chain[] = \base64_decode($header->x5c[$i]);
|
||||||
|
}
|
||||||
|
unset ($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
// Verify that the nonce in the response is identical to the Base64 encoding
|
||||||
|
// of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.
|
||||||
|
if (!$this->_payload->nonce || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) {
|
||||||
|
throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that attestationCert is issued to the hostname "attest.android.com"
|
||||||
|
$certInfo = \openssl_x509_parse($this->getCertificatePem());
|
||||||
|
if (!\is_array($certInfo) || !$certInfo['subject'] || $certInfo['subject']['CN'] !== 'attest.android.com') {
|
||||||
|
throw new WebAuthnException('invalid certificate CN in JWS (' . $certInfo['subject']['CN']. ')', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the ctsProfileMatch attribute in the payload of response is true.
|
||||||
|
if (!$this->_payload->ctsProfileMatch) {
|
||||||
|
throw new WebAuthnException('invalid ctsProfileMatch in payload', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* decode base64 url
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function _base64url_decode($data) {
|
||||||
|
return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
|
||||||
|
abstract class FormatBase {
|
||||||
|
protected $_attestationObject = null;
|
||||||
|
protected $_authenticatorData = null;
|
||||||
|
protected $_x5c_chain = array();
|
||||||
|
protected $_x5c_tempFile = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Array $AttestionObject
|
||||||
|
* @param \WebAuthn\Attestation\AuthenticatorData $authenticatorData
|
||||||
|
*/
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
$this->_attestationObject = $AttestionObject;
|
||||||
|
$this->_authenticatorData = $authenticatorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __destruct() {
|
||||||
|
// delete X.509 chain certificate file after use
|
||||||
|
if (\is_file($this->_x5c_tempFile)) {
|
||||||
|
\unlink($this->_x5c_tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the certificate chain in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificateChain() {
|
||||||
|
if (\is_file($this->_x5c_tempFile)) {
|
||||||
|
return \file_get_contents($this->_x5c_tempFile);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the key X.509 certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
// need to be overwritten
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks validity of the signature
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
// need to be overwritten
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
// need to be overwritten
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a PEM encoded certificate with X.509 binary data
|
||||||
|
* @param string $x5c
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function _createCertificatePem($x5c) {
|
||||||
|
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
|
||||||
|
$pem .= \chunk_split(\base64_encode($x5c), 64, "\n");
|
||||||
|
$pem .= '-----END CERTIFICATE-----' . "\n";
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a PEM encoded chain file
|
||||||
|
* @return type
|
||||||
|
*/
|
||||||
|
protected function _createX5cChainFile() {
|
||||||
|
$content = '';
|
||||||
|
if (\is_array($this->_x5c_chain) && \count($this->_x5c_chain) > 0) {
|
||||||
|
foreach ($this->_x5c_chain as $x5c) {
|
||||||
|
$certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c));
|
||||||
|
// check if issuer = subject (self signed)
|
||||||
|
if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) {
|
||||||
|
$selfSigned = true;
|
||||||
|
foreach ($certInfo['issuer'] as $k => $v) {
|
||||||
|
if ($certInfo['subject'][$k] !== $v) {
|
||||||
|
$selfSigned = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$selfSigned) {
|
||||||
|
$content .= "\n" . $this->_createCertificatePem($x5c) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($content) {
|
||||||
|
$this->_x5c_tempFile = \sys_get_temp_dir() . '/x5c_chain_' . \base_convert(\rand(), 10, 36) . '.pem';
|
||||||
|
if (\file_put_contents($this->_x5c_tempFile, $content) !== false) {
|
||||||
|
return $this->_x5c_tempFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the name and openssl key for provided cose number.
|
||||||
|
* @param int $coseNumber
|
||||||
|
* @return \stdClass|null
|
||||||
|
*/
|
||||||
|
protected function _getCoseAlgorithm($coseNumber) {
|
||||||
|
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||||
|
$coseAlgorithms = array(
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA1',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA1,
|
||||||
|
'cose' => array(
|
||||||
|
-65535 // RS1
|
||||||
|
)),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA256',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA256,
|
||||||
|
'cose' => array(
|
||||||
|
-257, // RS256
|
||||||
|
-37, // PS256
|
||||||
|
-7, // ES256
|
||||||
|
5 // HMAC256
|
||||||
|
)),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA384',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA384,
|
||||||
|
'cose' => array(
|
||||||
|
-258, // RS384
|
||||||
|
-38, // PS384
|
||||||
|
-35, // ES384
|
||||||
|
6 // HMAC384
|
||||||
|
)),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA512',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA512,
|
||||||
|
'cose' => array(
|
||||||
|
-259, // RS512
|
||||||
|
-39, // PS512
|
||||||
|
-36, // ES512
|
||||||
|
7 // HMAC512
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($coseAlgorithms as $coseAlgorithm) {
|
||||||
|
if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) {
|
||||||
|
$return = new \stdClass();
|
||||||
|
$return->hash = $coseAlgorithm['hash'];
|
||||||
|
$return->openssl = $coseAlgorithm['openssl'];
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
class None extends FormatBase {
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class Packed extends FormatBase {
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check packed data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
|
||||||
|
// certificate for validation
|
||||||
|
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||||
|
|
||||||
|
// The attestation certificate attestnCert MUST be the first element in the array
|
||||||
|
$attestnCert = array_shift($attStmt['x5c']);
|
||||||
|
|
||||||
|
if (!($attestnCert instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = $attestnCert->getBinaryString();
|
||||||
|
|
||||||
|
// certificate chain
|
||||||
|
foreach ($attStmt['x5c'] as $chain) {
|
||||||
|
if ($chain instanceof ByteBuffer) {
|
||||||
|
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
if ($this->_x5c) {
|
||||||
|
return $this->_validateOverX5c($clientDataHash);
|
||||||
|
} else {
|
||||||
|
return $this->_validateSelfAttestation($clientDataHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if x5c is present
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected function _validateOverX5c($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||||
|
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if self attestation is in use
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function _validateSelfAttestation($clientDataHash) {
|
||||||
|
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the credential public key with alg.
|
||||||
|
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$publicKey = $this->_authenticatorData->getPublicKeyPem();
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class Tpm extends FormatBase {
|
||||||
|
private $_TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
|
||||||
|
private $_TPM_ST_ATTEST_CERTIFY = "\x80\x17";
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_pubArea;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ByteBuffer
|
||||||
|
*/
|
||||||
|
private $_certInfo;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check packed data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') {
|
||||||
|
throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
$this->_certInfo = $attStmt['certInfo'];
|
||||||
|
$this->_pubArea = $attStmt['pubArea'];
|
||||||
|
|
||||||
|
// certificate for validation
|
||||||
|
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||||
|
|
||||||
|
// The attestation certificate attestnCert MUST be the first element in the array
|
||||||
|
$attestnCert = array_shift($attStmt['x5c']);
|
||||||
|
|
||||||
|
if (!($attestnCert instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = $attestnCert->getBinaryString();
|
||||||
|
|
||||||
|
// certificate chain
|
||||||
|
foreach ($attStmt['x5c'] as $chain) {
|
||||||
|
if ($chain instanceof ByteBuffer) {
|
||||||
|
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return $this->_validateOverX5c($clientDataHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if x5c is present
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected function _validateOverX5c($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate authenticatorData and clientDataHash to form attToBeSigned.
|
||||||
|
$attToBeSigned = $this->_authenticatorData->getBinary();
|
||||||
|
$attToBeSigned .= $clientDataHash;
|
||||||
|
|
||||||
|
// Validate that certInfo is valid:
|
||||||
|
|
||||||
|
// Verify that magic is set to TPM_GENERATED_VALUE.
|
||||||
|
if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) {
|
||||||
|
throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that type is set to TPM_ST_ATTEST_CERTIFY.
|
||||||
|
if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) {
|
||||||
|
throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = 6;
|
||||||
|
$qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
|
||||||
|
$extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
|
||||||
|
$coseAlg = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
|
||||||
|
if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) {
|
||||||
|
throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the sig is a valid signature over certInfo using the attestation
|
||||||
|
// public key in aikCert with the algorithm specified in alg.
|
||||||
|
return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns next part of ByteBuffer
|
||||||
|
* @param ByteBuffer $buffer
|
||||||
|
* @param int $offset
|
||||||
|
* @return ByteBuffer
|
||||||
|
*/
|
||||||
|
protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) {
|
||||||
|
$len = $buffer->getUint16Val($offset);
|
||||||
|
$data = $buffer->getBytes($offset + 2, $len);
|
||||||
|
$offset += (2 + $len);
|
||||||
|
|
||||||
|
return new ByteBuffer($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class U2f extends FormatBase {
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check u2f data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
$this->_x5c = $attStmt['x5c'][0]->getBinaryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
|
||||||
|
$pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n");
|
||||||
|
$pem .= '-----END CERTIFICATE-----' . "\n";
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
|
||||||
|
$dataToVerify = "\x00";
|
||||||
|
$dataToVerify .= $this->_authenticatorData->getRpIdHash();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
$dataToVerify .= $this->_authenticatorData->getCredentialId();
|
||||||
|
$dataToVerify .= $this->_authenticatorData->getPublicKeyU2F();
|
||||||
|
|
||||||
|
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Binary;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/ByteBuffer.php
|
||||||
|
* Copyright © 2018 Thomas Bleeker - MIT licensed
|
||||||
|
* Modified by Lukas Buchs
|
||||||
|
* Thanks Thomas for your work!
|
||||||
|
*/
|
||||||
|
class ByteBuffer implements \JsonSerializable, \Serializable {
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $useBase64UrlEncoding = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $_data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $_length;
|
||||||
|
|
||||||
|
public function __construct($binaryData) {
|
||||||
|
$this->_data = $binaryData;
|
||||||
|
$this->_length = \strlen($binaryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// PUBLIC STATIC
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a ByteBuffer from a base64 url encoded string
|
||||||
|
* @param string $base64url
|
||||||
|
* @return \WebAuthn\Binary\ByteBuffer
|
||||||
|
*/
|
||||||
|
public static function fromBase64Url($base64url) {
|
||||||
|
$bin = self::_base64url_decode($base64url);
|
||||||
|
if ($bin === false) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return new ByteBuffer($bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a ByteBuffer from a base64 url encoded string
|
||||||
|
* @param string $hex
|
||||||
|
* @return \WebAuthn\Binary\ByteBuffer
|
||||||
|
*/
|
||||||
|
public static function fromHex($hex) {
|
||||||
|
$bin = \hex2bin($hex);
|
||||||
|
if ($bin === false) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return new ByteBuffer($bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a random ByteBuffer
|
||||||
|
* @param string $length
|
||||||
|
* @return \WebAuthn\Binary\ByteBuffer
|
||||||
|
*/
|
||||||
|
public static function randomBuffer($length) {
|
||||||
|
if (\function_exists('random_bytes')) { // >PHP 7.0
|
||||||
|
return new ByteBuffer(\random_bytes($length));
|
||||||
|
|
||||||
|
} else if (\function_exists('openssl_random_pseudo_bytes')) {
|
||||||
|
return new ByteBuffer(\openssl_random_pseudo_bytes($length));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// PUBLIC
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
public function getBytes($offset, $length) {
|
||||||
|
if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return \substr($this->_data, $offset, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getByteVal($offset) {
|
||||||
|
if ($offset < 0 || $offset >= $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return \ord(\substr($this->_data, $offset, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLength() {
|
||||||
|
return $this->_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUint16Val($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 2) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return unpack('n', $this->_data, $offset)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUint32Val($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 4) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
$val = unpack('N', $this->_data, $offset)[1];
|
||||||
|
|
||||||
|
// Signed integer overflow causes signed negative numbers
|
||||||
|
if ($val < 0) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUint64Val($offset) {
|
||||||
|
if (PHP_INT_SIZE < 8) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
if ($offset < 0 || ($offset + 8) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
$val = unpack('J', $this->_data, $offset)[1];
|
||||||
|
|
||||||
|
// Signed integer overflow causes signed negative numbers
|
||||||
|
if ($val < 0) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHalfFloatVal($offset) {
|
||||||
|
//FROM spec pseudo decode_half(unsigned char *halfp)
|
||||||
|
$half = $this->getUint16Val($offset);
|
||||||
|
|
||||||
|
$exp = ($half >> 10) & 0x1f;
|
||||||
|
$mant = $half & 0x3ff;
|
||||||
|
|
||||||
|
if ($exp === 0) {
|
||||||
|
$val = $mant * (2 ** -24);
|
||||||
|
} elseif ($exp !== 31) {
|
||||||
|
$val = ($mant + 1024) * (2 ** ($exp - 25));
|
||||||
|
} else {
|
||||||
|
$val = ($mant === 0) ? INF : NAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($half & 0x8000) ? -$val : $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFloatVal($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 4) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return unpack('G', $this->_data, $offset)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDoubleVal($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 8) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return unpack('E', $this->_data, $offset)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBinaryString() {
|
||||||
|
return $this->_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $buffer
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function equals($buffer) {
|
||||||
|
return is_string($this->_data) && $this->_data === $buffer->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHex() {
|
||||||
|
return \bin2hex($this->_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEmpty() {
|
||||||
|
return $this->_length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsonSerialize interface
|
||||||
|
* return binary data in RFC 1342-Like serialized string
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
public function jsonSerialize() {
|
||||||
|
if (ByteBuffer::$useBase64UrlEncoding) {
|
||||||
|
return self::_base64url_encode($this->_data);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return '=?BINARY?B?' . \base64_encode($this->_data) . '?=';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable-Interface
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function serialize() {
|
||||||
|
return \serialize($this->_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable-Interface
|
||||||
|
* @param string $serialized
|
||||||
|
*/
|
||||||
|
public function unserialize($serialized) {
|
||||||
|
$this->_data = \unserialize($serialized);
|
||||||
|
$this->_length = \strlen($this->_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// PROTECTED STATIC
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* base64 url decoding
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function _base64url_decode($data) {
|
||||||
|
return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* base64 url encoding
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function _base64url_encode($data) {
|
||||||
|
return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\CBOR;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/CborDecoder.php
|
||||||
|
* Copyright © 2018 Thomas Bleeker - MIT licensed
|
||||||
|
* Modified by Lukas Buchs
|
||||||
|
* Thanks Thomas for your work!
|
||||||
|
*/
|
||||||
|
class CborDecoder {
|
||||||
|
const CBOR_MAJOR_UNSIGNED_INT = 0;
|
||||||
|
const CBOR_MAJOR_TEXT_STRING = 3;
|
||||||
|
const CBOR_MAJOR_FLOAT_SIMPLE = 7;
|
||||||
|
const CBOR_MAJOR_NEGATIVE_INT = 1;
|
||||||
|
const CBOR_MAJOR_ARRAY = 4;
|
||||||
|
const CBOR_MAJOR_TAG = 6;
|
||||||
|
const CBOR_MAJOR_MAP = 5;
|
||||||
|
const CBOR_MAJOR_BYTE_STRING = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ByteBuffer|string $bufOrBin
|
||||||
|
* @return mixed
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public static function decode($bufOrBin) {
|
||||||
|
$buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
|
||||||
|
|
||||||
|
$offset = 0;
|
||||||
|
$result = self::_parseItem($buf, $offset);
|
||||||
|
if ($offset !== $buf->getLength()) {
|
||||||
|
throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ByteBuffer|string $bufOrBin
|
||||||
|
* @param int $startOffset
|
||||||
|
* @param int|null $endOffset
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) {
|
||||||
|
$buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
|
||||||
|
|
||||||
|
$offset = $startOffset;
|
||||||
|
$data = self::_parseItem($buf, $offset);
|
||||||
|
$endOffset = $offset;
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// protected
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ByteBuffer $buf
|
||||||
|
* @param int $offset
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected static function _parseItem(ByteBuffer $buf, &$offset) {
|
||||||
|
$first = $buf->getByteVal($offset++);
|
||||||
|
$type = $first >> 5;
|
||||||
|
$val = $first & 0b11111;
|
||||||
|
|
||||||
|
if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) {
|
||||||
|
return self::_parseFloatSimple($val, $buf, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
$val = self::_parseExtraLength($val, $buf, $offset);
|
||||||
|
|
||||||
|
return self::_parseItemData($type, $val, $buf, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) {
|
||||||
|
switch ($val) {
|
||||||
|
case 24:
|
||||||
|
$val = $buf->getByteVal($offset);
|
||||||
|
$offset++;
|
||||||
|
return self::_parseSimple($val);
|
||||||
|
|
||||||
|
case 25:
|
||||||
|
$floatValue = $buf->getHalfFloatVal($offset);
|
||||||
|
$offset += 2;
|
||||||
|
return $floatValue;
|
||||||
|
|
||||||
|
case 26:
|
||||||
|
$floatValue = $buf->getFloatVal($offset);
|
||||||
|
$offset += 4;
|
||||||
|
return $floatValue;
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
$floatValue = $buf->getDoubleVal($offset);
|
||||||
|
$offset += 8;
|
||||||
|
return $floatValue;
|
||||||
|
|
||||||
|
case 28:
|
||||||
|
case 29:
|
||||||
|
case 30:
|
||||||
|
throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
|
||||||
|
|
||||||
|
case 31:
|
||||||
|
throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::_parseSimple($val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $val
|
||||||
|
* @return mixed
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected static function _parseSimple($val) {
|
||||||
|
if ($val === 20) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($val === 21) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($val === 22) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) {
|
||||||
|
switch ($val) {
|
||||||
|
case 24:
|
||||||
|
$val = $buf->getByteVal($offset);
|
||||||
|
$offset++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 25:
|
||||||
|
$val = $buf->getUint16Val($offset);
|
||||||
|
$offset += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 26:
|
||||||
|
$val = $buf->getUint32Val($offset);
|
||||||
|
$offset += 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
$val = $buf->getUint64Val($offset);
|
||||||
|
$offset += 8;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 28:
|
||||||
|
case 29:
|
||||||
|
case 30:
|
||||||
|
throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
|
||||||
|
|
||||||
|
case 31:
|
||||||
|
throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) {
|
||||||
|
switch ($type) {
|
||||||
|
case self::CBOR_MAJOR_UNSIGNED_INT: // uint
|
||||||
|
return $val;
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_NEGATIVE_INT:
|
||||||
|
return -1 - $val;
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_BYTE_STRING:
|
||||||
|
$data = $buf->getBytes($offset, $val);
|
||||||
|
$offset += $val;
|
||||||
|
return new ByteBuffer($data); // bytes
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_TEXT_STRING:
|
||||||
|
$data = $buf->getBytes($offset, $val);
|
||||||
|
$offset += $val;
|
||||||
|
return $data; // UTF-8
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_ARRAY:
|
||||||
|
return self::_parseArray($buf, $offset, $val);
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_MAP:
|
||||||
|
return self::_parseMap($buf, $offset, $val);
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_TAG:
|
||||||
|
return self::_parseItem($buf, $offset); // 1 embedded data item
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should never be reached
|
||||||
|
throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseMap(ByteBuffer $buf, &$offset, $count) {
|
||||||
|
$map = array();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$mapKey = self::_parseItem($buf, $offset);
|
||||||
|
$mapVal = self::_parseItem($buf, $offset);
|
||||||
|
|
||||||
|
if (!\is_int($mapKey) && !\is_string($mapKey)) {
|
||||||
|
throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[$mapKey] = $mapVal; // todo dup
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseArray(ByteBuffer $buf, &$offset, $count) {
|
||||||
|
$arr = array();
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$arr[] = self::_parseItem($buf, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
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.
|
|
@ -0,0 +1,487 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
require_once 'WebAuthnException.php';
|
||||||
|
require_once 'Binary/ByteBuffer.php';
|
||||||
|
require_once 'Attestation/AttestationObject.php';
|
||||||
|
require_once 'Attestation/AuthenticatorData.php';
|
||||||
|
require_once 'Attestation/Format/FormatBase.php';
|
||||||
|
require_once 'Attestation/Format/None.php';
|
||||||
|
require_once 'Attestation/Format/AndroidKey.php';
|
||||||
|
require_once 'Attestation/Format/AndroidSafetyNet.php';
|
||||||
|
require_once 'Attestation/Format/Packed.php';
|
||||||
|
require_once 'Attestation/Format/Tpm.php';
|
||||||
|
require_once 'Attestation/Format/U2f.php';
|
||||||
|
require_once 'CBOR/CborDecoder.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebAuthn
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class WebAuthn {
|
||||||
|
// relying party
|
||||||
|
private $_rpName;
|
||||||
|
private $_rpId;
|
||||||
|
private $_rpIdHash;
|
||||||
|
private $_challenge;
|
||||||
|
private $_signatureCounter;
|
||||||
|
private $_caFiles;
|
||||||
|
private $_formats;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new WebAuthn server
|
||||||
|
* @param string $rpName the relying party name
|
||||||
|
* @param string $rpId the relying party ID = the domain name
|
||||||
|
* @param bool $useBase64UrlEncoding true to use base64 url encoding for binary data in json objects. Default is a RFC 1342-Like serialized string.
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function __construct($rpName, $rpId, $allowedFormats=null, $useBase64UrlEncoding=false) {
|
||||||
|
$this->_rpName = $rpName;
|
||||||
|
$this->_rpId = $rpId;
|
||||||
|
$this->_rpIdHash = \hash('sha256', $rpId, true);
|
||||||
|
ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding;
|
||||||
|
|
||||||
|
if (!\function_exists('\openssl_open')) {
|
||||||
|
throw new WebAuthnException('OpenSSL-Module not installed');;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) {
|
||||||
|
throw new WebAuthnException('SHA256 not supported by this openssl installation.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// default value
|
||||||
|
if (!is_array($allowedFormats)) {
|
||||||
|
$allowedFormats = array('android-key', 'fido-u2f', 'packed', 'tpm');
|
||||||
|
}
|
||||||
|
$this->_formats = $allowedFormats;
|
||||||
|
|
||||||
|
// validate formats
|
||||||
|
$invalidFormats = \array_diff($this->_formats, array('android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm'));
|
||||||
|
if (!$this->_formats || $invalidFormats) {
|
||||||
|
throw new WebAuthnException('Invalid formats on construct: ' . implode(', ', $invalidFormats));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add a root certificate to verify new registrations
|
||||||
|
* @param string $path file path of / directory with root certificates
|
||||||
|
*/
|
||||||
|
public function addRootCertificates($path) {
|
||||||
|
if (!\is_array($this->_caFiles)) {
|
||||||
|
$this->_caFiles = array();
|
||||||
|
}
|
||||||
|
$path = \rtrim(\trim($path), '\\/');
|
||||||
|
if (\is_dir($path)) {
|
||||||
|
foreach (\scandir($path) as $ca) {
|
||||||
|
if (\is_file($path . '/' . $ca)) {
|
||||||
|
$this->addRootCertificates($path . '/' . $ca);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) {
|
||||||
|
$this->_caFiles[] = \realpath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the generated challenge to save for later validation
|
||||||
|
* @return ByteBuffer
|
||||||
|
*/
|
||||||
|
public function getChallenge() {
|
||||||
|
return $this->_challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates the object for a key registration
|
||||||
|
* provide this data to navigator.credentials.create
|
||||||
|
* @param string $userId
|
||||||
|
* @param string $userName
|
||||||
|
* @param string $userDisplayName
|
||||||
|
* @param int $timeout timeout in seconds
|
||||||
|
* @param bool $requireResidentKey true, if the key should be stored by the authentication device
|
||||||
|
* @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
|
||||||
|
* if the response does not have the UV flag set.
|
||||||
|
* Valid values:
|
||||||
|
* true = required
|
||||||
|
* false = preferred
|
||||||
|
* string 'required' 'preferred' 'discouraged'
|
||||||
|
* @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $excludeCredentialIds=array()) {
|
||||||
|
|
||||||
|
// validate User Verification Requirement
|
||||||
|
if (\is_bool($requireUserVerification)) {
|
||||||
|
$requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
|
||||||
|
} else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
|
||||||
|
$requireUserVerification = \strtolower($requireUserVerification);
|
||||||
|
} else {
|
||||||
|
$requireUserVerification = 'preferred';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = new \stdClass();
|
||||||
|
$args->publicKey = new \stdClass();
|
||||||
|
|
||||||
|
// relying party
|
||||||
|
$args->publicKey->rp = new \stdClass();
|
||||||
|
$args->publicKey->rp->name = $this->_rpName;
|
||||||
|
$args->publicKey->rp->id = $this->_rpId;
|
||||||
|
|
||||||
|
$args->publicKey->authenticatorSelection = new \stdClass();
|
||||||
|
$args->publicKey->authenticatorSelection->userVerification = $requireUserVerification;
|
||||||
|
if ($requireResidentKey) {
|
||||||
|
$args->publicKey->authenticatorSelection->requireResidentKey = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// user
|
||||||
|
$args->publicKey->user = new \stdClass();
|
||||||
|
$args->publicKey->user->id = new ByteBuffer($userId); // binary
|
||||||
|
$args->publicKey->user->name = $userName;
|
||||||
|
$args->publicKey->user->displayName = $userDisplayName;
|
||||||
|
|
||||||
|
$args->publicKey->pubKeyCredParams = array();
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$tmp->alg = -7; // ES256
|
||||||
|
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$tmp->alg = -257; // RS256
|
||||||
|
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
|
||||||
|
// if there are root certificates added, we need direct attestation to validate
|
||||||
|
// against the root certificate. If there are no root-certificates added,
|
||||||
|
// anonymization ca are also accepted, because we can't validate the root anyway.
|
||||||
|
$attestation = 'indirect';
|
||||||
|
if (\is_array($this->_caFiles)) {
|
||||||
|
$attestation = 'direct';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args->publicKey->attestation = \count($this->_formats) === 1 && \in_array('none', $this->_formats) ? 'none' : $attestation;
|
||||||
|
$args->publicKey->extensions = new \stdClass();
|
||||||
|
$args->publicKey->extensions->exts = true;
|
||||||
|
$args->publicKey->timeout = $timeout * 1000; // microseconds
|
||||||
|
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||||
|
|
||||||
|
//prevent re-registration by specifying existing credentials
|
||||||
|
$args->publicKey->excludeCredentials = array();
|
||||||
|
|
||||||
|
if (is_array($excludeCredentialIds)) {
|
||||||
|
foreach ($excludeCredentialIds as $id) {
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$tmp->transports = array('usb', 'ble', 'nfc', 'internal');
|
||||||
|
$args->publicKey->excludeCredentials[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates the object for key validation
|
||||||
|
* Provide this data to navigator.credentials.get
|
||||||
|
* @param array $credentialIds binary
|
||||||
|
* @param int $timeout timeout in seconds
|
||||||
|
* @param bool $allowUsb allow removable USB
|
||||||
|
* @param bool $allowNfc allow Near Field Communication (NFC)
|
||||||
|
* @param bool $allowBle allow Bluetooth
|
||||||
|
* @param bool $allowInternal allow client device-specific transport. These authenticators are not removable from the client device.
|
||||||
|
* @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
|
||||||
|
* if the response does not have the UV flag set.
|
||||||
|
* Valid values:
|
||||||
|
* true = required
|
||||||
|
* false = preferred
|
||||||
|
* string 'required' 'preferred' 'discouraged'
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
public function getGetArgs($credentialIds=array(), $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowInternal=true, $requireUserVerification=false) {
|
||||||
|
|
||||||
|
// validate User Verification Requirement
|
||||||
|
if (\is_bool($requireUserVerification)) {
|
||||||
|
$requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
|
||||||
|
} else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
|
||||||
|
$requireUserVerification = \strtolower($requireUserVerification);
|
||||||
|
} else {
|
||||||
|
$requireUserVerification = 'preferred';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = new \stdClass();
|
||||||
|
$args->publicKey = new \stdClass();
|
||||||
|
$args->publicKey->timeout = $timeout * 1000; // microseconds
|
||||||
|
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||||
|
$args->publicKey->userVerification = $requireUserVerification;
|
||||||
|
$args->publicKey->rpId = $this->_rpId;
|
||||||
|
|
||||||
|
if (\is_array($credentialIds) && \count($credentialIds) > 0) {
|
||||||
|
$args->publicKey->allowCredentials = array();
|
||||||
|
|
||||||
|
foreach ($credentialIds as $id) {
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||||
|
$tmp->transports = array();
|
||||||
|
|
||||||
|
if ($allowUsb) {
|
||||||
|
$tmp->transports[] = 'usb';
|
||||||
|
}
|
||||||
|
if ($allowNfc) {
|
||||||
|
$tmp->transports[] = 'nfc';
|
||||||
|
}
|
||||||
|
if ($allowBle) {
|
||||||
|
$tmp->transports[] = 'ble';
|
||||||
|
}
|
||||||
|
if ($allowInternal) {
|
||||||
|
$tmp->transports[] = 'internal';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$args->publicKey->allowCredentials[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the new signature counter value.
|
||||||
|
* returns null if there is no counter
|
||||||
|
* @return ?int
|
||||||
|
*/
|
||||||
|
public function getSignatureCounter() {
|
||||||
|
return \is_int($this->_signatureCounter) ? $this->_signatureCounter : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a create request and returns data to save for future logins
|
||||||
|
* @param string $clientDataJSON binary from browser
|
||||||
|
* @param string $attestationObject binary from browser
|
||||||
|
* @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)
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true) {
|
||||||
|
$clientDataHash = \hash('sha256', $clientDataJSON, true);
|
||||||
|
$clientData = \json_decode($clientDataJSON);
|
||||||
|
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
|
||||||
|
|
||||||
|
// security: https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||||
|
|
||||||
|
// 2. Let C, the client data claimed as collected during the credential creation,
|
||||||
|
// be the result of running an implementation-specific JSON parser on JSONtext.
|
||||||
|
if (!\is_object($clientData)) {
|
||||||
|
throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify that the value of C.type is webauthn.create.
|
||||||
|
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') {
|
||||||
|
throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
|
||||||
|
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||||
|
throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Verify that the value of C.origin matches the Relying Party's origin.
|
||||||
|
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||||
|
throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attestation
|
||||||
|
$attestationObject = new Attestation\AttestationObject($attestationObject, $this->_formats);
|
||||||
|
|
||||||
|
// 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
|
||||||
|
if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) {
|
||||||
|
throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature
|
||||||
|
if (!$attestationObject->validateAttestation($clientDataHash)) {
|
||||||
|
throw new WebAuthnException('Invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15. If validation is successful, obtain a list of acceptable trust anchors
|
||||||
|
if (is_array($this->_caFiles) && !$attestationObject->validateRootCertificate($this->_caFiles)) {
|
||||||
|
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()) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$signCount = $attestationObject->getAuthenticatorData()->getSignCount();
|
||||||
|
if ($signCount > 0) {
|
||||||
|
$this->_signatureCounter = $signCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare data to store for future logins
|
||||||
|
$data = new \stdClass();
|
||||||
|
$data->rpId = $this->_rpId;
|
||||||
|
$data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId();
|
||||||
|
$data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem();
|
||||||
|
$data->certificateChain = $attestationObject->getCertificateChain();
|
||||||
|
$data->certificate = $attestationObject->getCertificatePem();
|
||||||
|
$data->certificateIssuer = $attestationObject->getCertificateIssuer();
|
||||||
|
$data->certificateSubject = $attestationObject->getCertificateSubject();
|
||||||
|
$data->signatureCounter = $this->_signatureCounter;
|
||||||
|
$data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID();
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a get request
|
||||||
|
* @param string $clientDataJSON binary from browser
|
||||||
|
* @param string $authenticatorData binary from browser
|
||||||
|
* @param string $signature binary from browser
|
||||||
|
* @param string $credentialPublicKey string PEM-formated public key from used credentialId
|
||||||
|
* @param string|ByteBuffer $challenge binary from used challange
|
||||||
|
* @param int $prevSignatureCnt signature count value of the last login
|
||||||
|
* @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
|
||||||
|
* @param bool $requireUserPresent true, if the device must check user presence (e.g. by pressing a button)
|
||||||
|
* @return boolean true if get is successful
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, $prevSignatureCnt=null, $requireUserVerification=false, $requireUserPresent=true) {
|
||||||
|
$authenticatorObj = new Attestation\AuthenticatorData($authenticatorData);
|
||||||
|
$clientDataHash = \hash('sha256', $clientDataJSON, true);
|
||||||
|
$clientData = \json_decode($clientDataJSON);
|
||||||
|
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||||
|
|
||||||
|
// 1. If the allowCredentials option was given when this authentication ceremony was initiated,
|
||||||
|
// verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
|
||||||
|
// -> TO BE VERIFIED BY IMPLEMENTATION
|
||||||
|
|
||||||
|
// 2. If credential.response.userHandle is present, verify that the user identified
|
||||||
|
// by this value is the owner of the public key credential identified by credential.id.
|
||||||
|
// -> TO BE VERIFIED BY IMPLEMENTATION
|
||||||
|
|
||||||
|
// 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is
|
||||||
|
// inappropriate for your use case), look up the corresponding credential public key.
|
||||||
|
// -> TO BE LOOKED UP BY IMPLEMENTATION
|
||||||
|
|
||||||
|
// 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
||||||
|
if (!\is_object($clientData)) {
|
||||||
|
throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Verify that the value of C.type is the string webauthn.get.
|
||||||
|
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') {
|
||||||
|
throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Verify that the value of C.challenge matches the challenge that was sent to the
|
||||||
|
// authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
|
||||||
|
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||||
|
throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Verify that the value of C.origin matches the Relying Party's origin.
|
||||||
|
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||||
|
throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
||||||
|
if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) {
|
||||||
|
throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Verify that the User Present bit of the flags in authData is set
|
||||||
|
if ($requireUserPresent && !$authenticatorObj->getUserPresent()) {
|
||||||
|
throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
|
||||||
|
if ($requireUserVerification && !$authenticatorObj->getUserVerified()) {
|
||||||
|
throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Verify the values of the client extension outputs
|
||||||
|
// (extensions not implemented)
|
||||||
|
|
||||||
|
// 16. Using the credential public key looked up in step 3, verify that sig is a valid signature
|
||||||
|
// over the binary concatenation of authData and hash.
|
||||||
|
$dataToVerify = '';
|
||||||
|
$dataToVerify .= $authenticatorData;
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$publicKey = \openssl_pkey_get_public($credentialPublicKey);
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
||||||
|
throw new WebAuthnException('Invalid signature', WebAuthnException::INVALID_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17. If the signature counter value authData.signCount is nonzero,
|
||||||
|
// if less than or equal to the signature counter value stored,
|
||||||
|
// is a signal that the authenticator may be cloned
|
||||||
|
$signatureCounter = $authenticatorObj->getSignCount();
|
||||||
|
if ($signatureCounter > 0) {
|
||||||
|
$this->_signatureCounter = $signatureCounter;
|
||||||
|
if ($prevSignatureCnt !== null && $prevSignatureCnt >= $signatureCounter) {
|
||||||
|
throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// PRIVATE
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if the origin matchs the RP ID
|
||||||
|
* @param string $origin
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _checkOrigin($origin) {
|
||||||
|
// https://www.w3.org/TR/webauthn/#rp-id
|
||||||
|
|
||||||
|
// The origin's scheme must be https
|
||||||
|
if ($this->_rpId !== 'localhost' && \parse_url($origin, PHP_URL_SCHEME) !== 'https') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract host from origin
|
||||||
|
$host = \parse_url($origin, PHP_URL_HOST);
|
||||||
|
$host = \trim($host, '.');
|
||||||
|
|
||||||
|
// The RP ID must be equal to the origin's effective domain, or a registrable
|
||||||
|
// domain suffix of the origin's effective domain.
|
||||||
|
return \preg_match('/' . \preg_quote($this->_rpId) . '$/i', $host) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates a new challange
|
||||||
|
* @param int $length
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _createChallenge($length = 32) {
|
||||||
|
if (!$this->_challenge) {
|
||||||
|
$this->_challenge = ByteBuffer::randomBuffer($length);
|
||||||
|
}
|
||||||
|
return $this->_challenge;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
namespace WebAuthn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class WebAuthnException extends \Exception {
|
||||||
|
const INVALID_DATA = 1;
|
||||||
|
const INVALID_TYPE = 2;
|
||||||
|
const INVALID_CHALLENGE = 3;
|
||||||
|
const INVALID_ORIGIN = 4;
|
||||||
|
const INVALID_RELYING_PARTY = 5;
|
||||||
|
const INVALID_SIGNATURE = 6;
|
||||||
|
const INVALID_PUBLIC_KEY = 7;
|
||||||
|
const CERTIFICATE_NOT_TRUSTED = 8;
|
||||||
|
const USER_PRESENT = 9;
|
||||||
|
const USER_VERIFICATED = 10;
|
||||||
|
const SIGNATURE_COUNTER = 11;
|
||||||
|
const CRYPTO_STRONG = 13;
|
||||||
|
const BYTEBUFFER = 14;
|
||||||
|
const CBOR = 15;
|
||||||
|
|
||||||
|
public function __construct($message = "", $code = 0, $previous = null) {
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
68:1d:01:6c:7a:3c:e3:02:25:a5:01:94:28:47:57:71
|
||||||
|
|
||||||
|
Signature Algorithm: ecdsa-with-SHA384
|
||||||
|
|
||||||
|
Issuer:
|
||||||
|
stateOrProvinceName = California
|
||||||
|
organizationName = Apple Inc.
|
||||||
|
commonName = Apple WebAuthn Root CA
|
||||||
|
|
||||||
|
Validity
|
||||||
|
Not Before: Mar 18 18:21:32 2020 GMT
|
||||||
|
Not After : Mar 15 00:00:00 2045 GMT
|
||||||
|
|
||||||
|
Subject:
|
||||||
|
stateOrProvinceName = California
|
||||||
|
organizationName = Apple Inc.
|
||||||
|
commonName = Apple WebAuthn Root CA
|
||||||
|
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: id-ecPublicKey
|
||||||
|
ASN1 OID: secp384r1
|
||||||
|
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
26:D7:64:D9:C5:78:C2:5A:67:D1:A7:DE:6B:12:D0:1B:63:F1:C6:D7
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Certificate Sign, CRL Sign
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
|
||||||
|
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
|
||||||
|
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
|
||||||
|
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
|
||||||
|
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
|
||||||
|
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
|
||||||
|
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
|
||||||
|
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
|
||||||
|
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
|
||||||
|
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
|
||||||
|
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
||||||
|
1bWeT0vT
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,37 @@
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
04:00:00:00:00:01:0f:86:26:e6:0d
|
||||||
|
Signature Algorithm: sha1WithRSAEncryption
|
||||||
|
Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
|
||||||
|
Validity
|
||||||
|
Not Before: Dec 15 08:00:00 2006 GMT
|
||||||
|
Not After : Dec 15 08:00:00 2021 GMT
|
||||||
|
Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (2048 bit)
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
|
||||||
|
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
|
||||||
|
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
|
||||||
|
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
|
||||||
|
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
|
||||||
|
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
|
||||||
|
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
|
||||||
|
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
|
||||||
|
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
|
||||||
|
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
|
||||||
|
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
|
||||||
|
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
|
||||||
|
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
|
||||||
|
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
|
||||||
|
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
|
||||||
|
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
|
||||||
|
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
|
||||||
|
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
|
||||||
|
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||||
|
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,130 @@
|
||||||
|
Google Hardware Attestation Root certificate
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
https://developer.android.com/training/articles/security-key-attestation.html
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
e8:fa:19:63:14:d2:fa:18
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer: serialNumber = f92009e853b6b045
|
||||||
|
Validity
|
||||||
|
Not Before: May 26 16:28:52 2016 GMT
|
||||||
|
Not After : May 24 16:28:52 2026 GMT
|
||||||
|
Subject: serialNumber = f92009e853b6b045
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (4096 bit)
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Digital Signature, Certificate Sign, CRL Sign
|
||||||
|
X509v3 CRL Distribution Points:
|
||||||
|
|
||||||
|
Full Name:
|
||||||
|
URI:https://android.googleapis.com/attestation/crl/
|
||||||
|
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||||
|
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
|
||||||
|
ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
|
||||||
|
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
|
||||||
|
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
|
||||||
|
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
|
||||||
|
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
|
||||||
|
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
|
||||||
|
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
|
||||||
|
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
|
||||||
|
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
|
||||||
|
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
|
||||||
|
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
|
||||||
|
AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
|
||||||
|
VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
|
||||||
|
BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
|
||||||
|
Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
|
||||||
|
ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
|
||||||
|
Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
|
||||||
|
qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
|
||||||
|
DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
|
||||||
|
QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
|
||||||
|
JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
|
||||||
|
CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
|
||||||
|
ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
|
||||||
|
qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
|
||||||
|
MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
|
||||||
|
wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number: 15352756130135856819 (0xd50ff25ba3f2d6b3)
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer:
|
||||||
|
serialNumber = f92009e853b6b045
|
||||||
|
Validity
|
||||||
|
Not Before: Nov 22 20:37:58 2019 GMT
|
||||||
|
Not After : Nov 18 20:37:58 2034 GMT
|
||||||
|
Subject:
|
||||||
|
serialNumber = f92009e853b6b045
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (4096 bit)
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Certificate Sign
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||||
|
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
|
||||||
|
NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
|
||||||
|
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
|
||||||
|
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
|
||||||
|
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
|
||||||
|
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
|
||||||
|
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
|
||||||
|
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
|
||||||
|
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
|
||||||
|
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
|
||||||
|
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
|
||||||
|
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
|
||||||
|
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
|
||||||
|
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
|
||||||
|
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
|
||||||
|
XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
|
||||||
|
h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
|
||||||
|
L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
|
||||||
|
QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
|
||||||
|
D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
|
||||||
|
mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
|
||||||
|
Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
|
||||||
|
oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
|
||||||
|
jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
|
||||||
|
ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
|
||||||
|
ex0SdDrx+tWUDqG8At2JHA==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,31 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFZDCCA0ygAwIBAgIIYsLLTehAXpYwDQYJKoZIhvcNAQELBQAwUDELMAkGA1UE
|
||||||
|
BhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEbMBkG
|
||||||
|
A1UEAwwSSHVhd2VpIENCRyBSb290IENBMB4XDTE3MDgyMTEwNTYyN1oXDTQyMDgx
|
||||||
|
NTEwNTYyN1owUDELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
|
||||||
|
CwwKSHVhd2VpIENCRzEbMBkGA1UEAwwSSHVhd2VpIENCRyBSb290IENBMIICIjAN
|
||||||
|
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1OyKm3Ig/6eibB7Uz2o93UqGk2M7
|
||||||
|
84WdfF8mvffvu218d61G5M3Px54E3kefUTk5Ky1ywHvw7Rp9KDuYv7ktaHkk+yr5
|
||||||
|
9Ihseu3a7iM/C6SnMSGt+LfB/Bcob9Abw95EigXQ4yQddX9hbNrin3AwZw8wMjEI
|
||||||
|
SYYDo5GuYDL0NbAiYg2Y5GpfYIqRzoi6GqDz+evLrsl20kJeCEPgJZN4Jg00Iq9k
|
||||||
|
++EKOZ5Jc/Zx22ZUgKpdwKABkvzshEgG6WWUPB+gosOiLv++inu/9blDpEzQZhjZ
|
||||||
|
9WVHpURHDK1YlCvubVAMhDpnbqNHZ0AxlPletdoyugrH/OLKl5inhMXNj3Re7Hl8
|
||||||
|
WsBWLUKp6sXFf0dvSFzqnr2jkhicS+K2IYZnjghC9cOBRO8fnkonh0EBt0evjUIK
|
||||||
|
r5ClbCKioBX8JU+d4ldtWOpp2FlxeFTLreDJ5ZBU4//bQpTwYMt7gwMK+MO5Wtok
|
||||||
|
Ux3UF98Z6GdUgbl6nBjBe82c7oIQXhHGHPnURQO7DDPgyVnNOnTPIkmiHJh/e3vk
|
||||||
|
VhiZNHFCCLTip6GoJVrLxwb9i4q+d0thw4doxVJ5NB9OfDMV64/ybJgpf7m3Ld2y
|
||||||
|
E0gsf1prrRlDFDXjlYyqqpf1l9Y0u3ctXo7UpXMgbyDEpUQhq3a7txZQO/17luTD
|
||||||
|
oA6Tz1ADavvBwHkCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
|
||||||
|
MAMBAf8wHQYDVR0OBBYEFKrE03lH6G4ja+/wqWwicz16GWmhMA0GCSqGSIb3DQEB
|
||||||
|
CwUAA4ICAQC1d3TMB+VHZdGrWJbfaBShFNiCTN/MceSHOpzBn6JumQP4N7mxCOwd
|
||||||
|
RSsGKQxV2NPH7LTXWNhUvUw5Sek96FWx/+Oa7jsj3WNAVtmS3zKpCQ5iGb08WIRO
|
||||||
|
cFnx3oUQ5rcO8r/lUk7Q2cN0E+rF4xsdQrH9k2cd3kAXZXBjfxfKPJTdPy1XnZR/
|
||||||
|
h8H5EwEK5DWjSzK1wKd3G/Fxdm3E23pcr4FZgdYdOlFSiqW2TJ3Qe6lF4GOKOOyd
|
||||||
|
WHkpu54ieTsqoYcuMKnKMjT2SLNNgv9Gu5ipaG8Olz6g9C7Htp943lmK/1Vtnhgg
|
||||||
|
pL3rDTsFX/+ehk7OtxuNzRMD9lXUtEfok7f8XB0dcL4ZjnEhDmp5QZqC1kMubHQt
|
||||||
|
QnTauEiv0YkSGOwJAUZpK1PIff5GgxXYfaHfBC6Op4q02ppl5Q3URl7XIjYLjvs9
|
||||||
|
t4S9xPe8tb6416V2fe1dZ62vOXMMKHkZjVihh+IceYpJYHuyfKoYJyahLOQXZykG
|
||||||
|
K5iPAEEtq3HPfMVF43RKHOwfhrAH5KwelUA/0EkcR4Gzth1MKEqojdnYNemkkSy7
|
||||||
|
aNPPT4LEm5R7sV6vG1CjwbgvQrWCgc4nMb8ngdfnVF7Ydqjqi9SAqUzIk4+Uf0ZY
|
||||||
|
+6RY5IcHdCaiPaWIE1xURQ8B0DRUURsQwXdjZhgLN/DKJpCl5aCCxg==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,56 @@
|
||||||
|
HyperFIDO U2F Security Key Attestation CA
|
||||||
|
https://hypersecu.com/support/downloads/attestation
|
||||||
|
|
||||||
|
Last Update: 2017-01-01
|
||||||
|
|
||||||
|
HyperFIDO U2F Security Key devices which contain attestation certificates signed by a set of CAs.
|
||||||
|
This file contains the CA certificates that Relying Parties (RP) need to configure their software
|
||||||
|
with to be able to verify U2F device certificates.
|
||||||
|
|
||||||
|
The file will be updated as needed when we publish more CA certificates.
|
||||||
|
|
||||||
|
Issuer: CN=FT FIDO 0100
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBjTCCATOgAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxGVCBGSURP
|
||||||
|
IDAxMDAwHhcNMTQwNzAxMTUzNjI2WhcNNDQwNzAzMTUzNjI2WjAXMRUwEwYDVQQD
|
||||||
|
EwxGVCBGSURPIDAxMDAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxdLxJx8ol
|
||||||
|
S3DS5cIHzunPF0gg69d+o8ZVCMJtpRtlfBzGuVL4YhaXk2SC2gptPTgmpZCV2vbN
|
||||||
|
fAPi5gOF0vbZo3AwbjAdBgNVHQ4EFgQUXt4jWlYDgwhaPU+EqLmeM9LoPRMwPwYD
|
||||||
|
VR0jBDgwNoAUXt4jWlYDgwhaPU+EqLmeM9LoPROhG6QZMBcxFTATBgNVBAMTDEZU
|
||||||
|
IEZJRE8gMDEwMIIBATAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQC2
|
||||||
|
D9o9cconKTo8+4GZPyZBJ3amc8F0/kzyidX9dhrAIAIgM9ocs5BW/JfmshVP9Mb+
|
||||||
|
Joa/kgX4dWbZxrk0ioTfJZg=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number: 4107 (0x100b)
|
||||||
|
Signature Algorithm: ecdsa-with-SHA256
|
||||||
|
Issuer:
|
||||||
|
commonName = HYPERFIDO 0200
|
||||||
|
organizationName = HYPERSECU
|
||||||
|
countryName = CA
|
||||||
|
Validity
|
||||||
|
Not Before: Jan 1 00:00:00 2018 GMT
|
||||||
|
Not After : Dec 31 23:59:59 2047 GMT
|
||||||
|
Subject:
|
||||||
|
commonName = HYPERFIDO 0200
|
||||||
|
organizationName = HYPERSECU
|
||||||
|
countryName = CA
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBxzCCAWygAwIBAgICEAswCgYIKoZIzj0EAwIwOjELMAkGA1UEBhMCQ0ExEjAQ
|
||||||
|
BgNVBAoMCUhZUEVSU0VDVTEXMBUGA1UEAwwOSFlQRVJGSURPIDAyMDAwIBcNMTgw
|
||||||
|
MTAxMDAwMDAwWhgPMjA0NzEyMzEyMzU5NTlaMDoxCzAJBgNVBAYTAkNBMRIwEAYD
|
||||||
|
VQQKDAlIWVBFUlNFQ1UxFzAVBgNVBAMMDkhZUEVSRklETyAwMjAwMFkwEwYHKoZI
|
||||||
|
zj0CAQYIKoZIzj0DAQcDQgAErKUI1G0S7a6IOLlmHipLlBuxTYjsEESQvzQh3dB7
|
||||||
|
dvxxWWm7kWL91rq6S7ayZG0gZPR+zYqdFzwAYDcG4+aX66NgMF4wHQYDVR0OBBYE
|
||||||
|
FLZYcfMMwkQAGbt3ryzZFPFypmsIMB8GA1UdIwQYMBaAFLZYcfMMwkQAGbt3ryzZ
|
||||||
|
FPFypmsIMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMC
|
||||||
|
A0kAMEYCIQCG2/ppMGt7pkcRie5YIohS3uDPIrmiRcTjqDclKVWg0gIhANcPNDZH
|
||||||
|
E2/zZ+uB5ThG9OZus+xSb4knkrbAyXKX2zm/
|
||||||
|
-----END CERTIFICATE-----
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,41 @@
|
||||||
|
Solokeys FIDO2/U2F Device Attestation CA
|
||||||
|
========================================
|
||||||
|
Data:
|
||||||
|
Version: 1 (0x0)
|
||||||
|
Serial Number: 14143382635911888524 (0xc44763928ff4be8c)
|
||||||
|
Signature Algorithm: ecdsa-with-SHA256
|
||||||
|
|
||||||
|
Issuer:
|
||||||
|
emailAddress = hello@solokeys.com
|
||||||
|
commonName = solokeys.com
|
||||||
|
organizationalUnitName = Root CA
|
||||||
|
organizationName = Solo Keys
|
||||||
|
stateOrProvinceName = Maryland
|
||||||
|
countryName = US
|
||||||
|
|
||||||
|
Validity
|
||||||
|
Not Before: Nov 11 12:51:42 2018 GMT
|
||||||
|
Not After : Oct 29 12:51:42 2068 GMT
|
||||||
|
|
||||||
|
Subject:
|
||||||
|
emailAddress = hello@solokeys.com
|
||||||
|
commonName = solokeys.com
|
||||||
|
organizationalUnitName = Root CA
|
||||||
|
organizationName = Solo Keys
|
||||||
|
stateOrProvinceName = Maryland
|
||||||
|
countryName = US
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB9DCCAZoCCQDER2OSj/S+jDAKBggqhkjOPQQDAjCBgDELMAkGA1UEBhMCVVMx
|
||||||
|
ETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsM
|
||||||
|
B1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYS
|
||||||
|
aGVsbG9Ac29sb2tleXMuY29tMCAXDTE4MTExMTEyNTE0MloYDzIwNjgxMDI5MTI1
|
||||||
|
MTQyWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQK
|
||||||
|
DAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlz
|
||||||
|
LmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMFkwEwYHKoZI
|
||||||
|
zj0CAQYIKoZIzj0DAQcDQgAEWHAN0CCJVZdMs0oktZ5m93uxmB1iyq8ELRLtqVFL
|
||||||
|
SOiHQEab56qRTB/QzrpGAY++Y2mw+vRuQMNhBiU0KzwjBjAKBggqhkjOPQQDAgNI
|
||||||
|
ADBFAiEAz9SlrAXIlEu87vra54rICPs+4b0qhp3PdzcTg7rvnP0CIGjxzlteQQx+
|
||||||
|
jQGd7rwSZuE5RWUPVygYhUstQO9zNUOs
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,42 @@
|
||||||
|
Yubico U2F Device Attestation CA
|
||||||
|
================================
|
||||||
|
|
||||||
|
Last Update: 2014-09-01
|
||||||
|
|
||||||
|
Yubico manufacturer U2F devices that contains device attestation
|
||||||
|
certificates signed by a set of Yubico CAs. This file contains the CA
|
||||||
|
certificates that Relying Parties (RP) need to configure their
|
||||||
|
software with to be able to verify U2F device certificates.
|
||||||
|
|
||||||
|
This file has been signed with OpenPGP and you should verify the
|
||||||
|
signature and the authenticity of the public key before trusting the
|
||||||
|
content. The signature is located next to the file:
|
||||||
|
|
||||||
|
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt
|
||||||
|
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt.sig
|
||||||
|
|
||||||
|
We will update this file from time to time when we publish more CA
|
||||||
|
certificates.
|
||||||
|
|
||||||
|
Name: Yubico U2F Root CA Serial 457200631
|
||||||
|
Issued: 2014-08-01
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
|
||||||
|
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
|
||||||
|
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
|
||||||
|
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||||
|
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
|
||||||
|
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
|
||||||
|
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
|
||||||
|
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
|
||||||
|
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
|
||||||
|
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
|
||||||
|
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
|
||||||
|
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
|
||||||
|
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
|
||||||
|
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
|
||||||
|
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
|
||||||
|
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -20,6 +20,9 @@ header_remove("X-Powered-By");
|
||||||
// Yubi OTP API
|
// Yubi OTP API
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/Yubico.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/Yubico.php';
|
||||||
|
|
||||||
|
// WebAuthn
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/WebAuthn.php';
|
||||||
|
|
||||||
// Autoload composer
|
// Autoload composer
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
|
||||||
|
|
||||||
|
@ -52,6 +55,18 @@ $u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']);
|
||||||
$qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
|
$qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
|
||||||
$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
|
$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/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');
|
||||||
|
|
||||||
// Redis
|
// Redis
|
||||||
$redis = new Redis();
|
$redis = new Redis();
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -89,6 +89,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||||
if (isset($_POST["unset_tfa_key"])) {
|
if (isset($_POST["unset_tfa_key"])) {
|
||||||
unset_tfa_key($_POST);
|
unset_tfa_key($_POST);
|
||||||
}
|
}
|
||||||
|
if (isset($_POST["unset_fido2_key"])) {
|
||||||
|
fido2(array("action" => "unset_fido2_key", "post_data" => $_POST));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
|
||||||
// TODO: Move file upload to API?
|
// TODO: Move file upload to API?
|
||||||
|
|
|
@ -173,6 +173,13 @@ $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';
|
||||||
// Show last IMAP and POP3 logins
|
// Show last IMAP and POP3 logins
|
||||||
$SHOW_LAST_LOGIN = true;
|
$SHOW_LAST_LOGIN = true;
|
||||||
|
|
||||||
|
// UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins
|
||||||
|
// true = required
|
||||||
|
// false = preferred
|
||||||
|
// string 'required' 'preferred' 'discouraged'
|
||||||
|
$FIDO2_UV_FLAG = 'preferred';
|
||||||
|
$FIDO2_USER_PRESENT_FLAG = true;
|
||||||
|
$FIDO2_FORMATS = array('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
|
// Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
|
||||||
$RSPAMD_MAPS = array(
|
$RSPAMD_MAPS = array(
|
||||||
|
|
|
@ -59,7 +59,16 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<div class="btn-group">
|
||||||
<button type="submit" class="btn btn-success" value="Login"><?= $lang['login']['login']; ?></button>
|
<button type="submit" class="btn btn-success" value="Login"><?= $lang['login']['login']; ?></button>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
|
<?= $lang['login']['other_logins']; ?> <span class="caret"></span></button>
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li><a href="#" id="fido2-login"><?= $lang['login']['fido2_webauthn']; ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php if(!isset($_SESSION['oauth2_request'])) { ?>
|
<?php if(!isset($_SESSION['oauth2_request'])) { ?>
|
||||||
<div class="btn-group pull-right">
|
<div class="btn-group pull-right">
|
||||||
<button type="button" <?=(isset($_SESSION['mailcow_locale']) && count($AVAILABLE_LANGUAGES) === 1) ? 'disabled="true"' : '' ?> class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button type="button" <?=(isset($_SESSION['mailcow_locale']) && count($AVAILABLE_LANGUAGES) === 1) ? 'disabled="true"' : '' ?> class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
@ -81,6 +90,7 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||||
?>
|
?>
|
||||||
<p><div class="alert alert-info"><?= sprintf($lang['login']['delayed'], $_SESSION['ldelay']); ?></b></div></p>
|
<p><div class="alert alert-info"><?= sprintf($lang['login']['delayed'], $_SESSION['ldelay']); ?></b></div></p>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
<div id="fido2-alerts"></div>
|
||||||
<?php if(!isset($_SESSION['oauth2_request'])) { ?>
|
<?php if(!isset($_SESSION['oauth2_request'])) { ?>
|
||||||
<legend><span class="glyphicon glyphicon-link" aria-hidden="true"></span> <?=$UI_TEXTS['apps_name'];?></legend>
|
<legend><span class="glyphicon glyphicon-link" aria-hidden="true"></span> <?=$UI_TEXTS['apps_name'];?></legend>
|
||||||
<?php
|
<?php
|
||||||
|
|
|
@ -453,6 +453,14 @@ jQuery(function($){
|
||||||
$('#priv_key_pre').text(decoded_key);
|
$('#priv_key_pre').text(decoded_key);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// FIDO2 friendly name modal
|
||||||
|
$('#fido2ChangeFn').on('show.bs.modal', function (e) {
|
||||||
|
rename_link = $(e.relatedTarget)
|
||||||
|
if (rename_link != null) {
|
||||||
|
$('#fido2_subject').val(rename_link.data('subject'));
|
||||||
|
$('#fido2_subject_desc').text(Base64.decode(rename_link.data('subject')));
|
||||||
|
}
|
||||||
|
})
|
||||||
// App links
|
// App links
|
||||||
function add_table_row(table_id, type) {
|
function add_table_row(table_id, type) {
|
||||||
var row = $('<tr />');
|
var row = $('<tr />');
|
||||||
|
|
|
@ -44,7 +44,6 @@ function api_log($_data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
|
|
||||||
if (isset($_GET['query'])) {
|
if (isset($_GET['query'])) {
|
||||||
|
|
||||||
$query = explode('/', $_GET['query']);
|
$query = explode('/', $_GET['query']);
|
||||||
|
@ -118,12 +117,14 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
echo isset($_SESSION['return']) ? json_encode($_SESSION['return']) : $generic_success;
|
echo isset($_SESSION['return']) ? json_encode($_SESSION['return']) : $generic_success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isset($_POST['attr'])) {
|
if (!isset($_POST['attr']) && $category != "fido2-registration") {
|
||||||
echo $request_incomplete;
|
echo $request_incomplete;
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if ($category != "fido2-registration") {
|
||||||
$attr = (array)json_decode($_POST['attr'], true);
|
$attr = (array)json_decode($_POST['attr'], true);
|
||||||
|
}
|
||||||
unset($attr['csrf_token']);
|
unset($attr['csrf_token']);
|
||||||
}
|
}
|
||||||
// only allow POST requests to POST API endpoints
|
// only allow POST requests to POST API endpoints
|
||||||
|
@ -137,6 +138,38 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($category) {
|
switch ($category) {
|
||||||
|
// fido2-registration via POST
|
||||||
|
case "fido2-registration":
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if (isset($_SESSION["mailcow_cc_role"]) && ($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin")) {
|
||||||
|
$post = trim(file_get_contents('php://input'));
|
||||||
|
if ($post) {
|
||||||
|
$post = json_decode($post);
|
||||||
|
}
|
||||||
|
$clientDataJSON = base64_decode($post->clientDataJSON);
|
||||||
|
$attestationObject = base64_decode($post->attestationObject);
|
||||||
|
$challenge = $_SESSION['challenge'];
|
||||||
|
try {
|
||||||
|
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $GLOBALS['FIDO2_UV_FLAG'], $GLOBALS['FIDO2_USER_PRESENT_FLAG']);
|
||||||
|
}
|
||||||
|
catch (Throwable $ex) {
|
||||||
|
$return = new stdClass();
|
||||||
|
$return->success = false;
|
||||||
|
$return->msg = $ex->getMessage();
|
||||||
|
echo json_encode($return);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
fido2(array("action" => "register", "registration" => $data));
|
||||||
|
$return = new stdClass();
|
||||||
|
$return->success = true;
|
||||||
|
echo json_encode($return);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo $request_incomplete;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "time_limited_alias":
|
case "time_limited_alias":
|
||||||
process_add_return(mailbox('add', 'time_limited_alias', $attr));
|
process_add_return(mailbox('add', 'time_limited_alias', $attr));
|
||||||
break;
|
break;
|
||||||
|
@ -222,6 +255,67 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "process":
|
||||||
|
// only allow POST requests to process API endpoints
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(array(
|
||||||
|
'type' => 'error',
|
||||||
|
'msg' => 'only POST method is allowed'
|
||||||
|
));
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
switch ($category) {
|
||||||
|
case "fido2-args":
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$post = trim(file_get_contents('php://input'));
|
||||||
|
if ($post) {
|
||||||
|
$post = json_decode($post);
|
||||||
|
}
|
||||||
|
$clientDataJSON = base64_decode($post->clientDataJSON);
|
||||||
|
$authenticatorData = base64_decode($post->authenticatorData);
|
||||||
|
$signature = base64_decode($post->signature);
|
||||||
|
$id = base64_decode($post->id);
|
||||||
|
$challenge = $_SESSION['challenge'];
|
||||||
|
$process_fido2 = fido2(array("action" => "get_pub_key", "cid" => $post->id));
|
||||||
|
if ($process_fido2['pub_key'] === false) {
|
||||||
|
$return = new stdClass();
|
||||||
|
$return->success = false;
|
||||||
|
echo json_encode($return);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_fido2['pub_key'], $challenge, null, $GLOBALS['FIDO2_UV_FLAG'], $GLOBALS['FIDO2_USER_PRESENT_FLAG']);
|
||||||
|
}
|
||||||
|
catch (Throwable $ex) {
|
||||||
|
unset($process_fido2);
|
||||||
|
$return = new stdClass();
|
||||||
|
$return->success = false;
|
||||||
|
echo json_encode($return);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$return = new stdClass();
|
||||||
|
$return->success = true;
|
||||||
|
$_SESSION["fido2_subject"] = $process_fido2['key_id'];
|
||||||
|
$stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(':username' => $process_fido2['username']));
|
||||||
|
$obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($obj_props['superadmin'] === 1) {
|
||||||
|
$_SESSION["mailcow_cc_role"] = "admin";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$_SESSION["mailcow_cc_role"] = "domainadmin";
|
||||||
|
}
|
||||||
|
$_SESSION["mailcow_cc_username"] = $process_fido2['username'];
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array("fido2_login"),
|
||||||
|
'msg' => array('logged_in_as', $process_fido2['username'])
|
||||||
|
);
|
||||||
|
echo json_encode($return);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "get":
|
case "get":
|
||||||
function process_get_return($data) {
|
function process_get_return($data) {
|
||||||
echo (!isset($data) || empty($data)) ? '{}' : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
echo (!isset($data) || empty($data)) ? '{}' : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
@ -238,7 +332,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
switch ($category) {
|
switch ($category) {
|
||||||
case "u2f-registration":
|
case "u2f-registration":
|
||||||
header('Content-Type: application/javascript');
|
header('Content-Type: application/javascript');
|
||||||
if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) {
|
if (isset($_SESSION["mailcow_cc_role"]) &&
|
||||||
|
($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") &&
|
||||||
|
$_SESSION["mailcow_cc_username"] == $object) {
|
||||||
list($req, $sigs) = $u2f->getRegisterData(get_u2f_registrations($object));
|
list($req, $sigs) = $u2f->getRegisterData(get_u2f_registrations($object));
|
||||||
$_SESSION['regReq'] = json_encode($req);
|
$_SESSION['regReq'] = json_encode($req);
|
||||||
$_SESSION['regSigs'] = json_encode($sigs);
|
$_SESSION['regSigs'] = json_encode($sigs);
|
||||||
|
@ -252,6 +348,23 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
// fido2-registration via GET
|
||||||
|
case "fido2-registration":
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if (isset($_SESSION["mailcow_cc_role"]) &&
|
||||||
|
($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") &&
|
||||||
|
$_SESSION["mailcow_cc_username"] == $object) {
|
||||||
|
// 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'], $excludeCredentialIds);
|
||||||
|
print(json_encode($createArgs));
|
||||||
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "u2f-authentication":
|
case "u2f-authentication":
|
||||||
header('Content-Type: application/javascript');
|
header('Content-Type: application/javascript');
|
||||||
if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
|
if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
|
||||||
|
@ -274,6 +387,19 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "fido2-get-args":
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
// Login without username, no ids!
|
||||||
|
// $ids = fido2(array("action" => "get_all_cids"));
|
||||||
|
// if (count($ids) == 0) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
$ids = NULL;
|
||||||
|
$getArgs = $WebAuthn->getGetArgs($ids, 30, true, true, true, true, $GLOBALS['FIDO2_UV_FLAG']);
|
||||||
|
print(json_encode($getArgs));
|
||||||
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
|
return;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (!isset($_SESSION['pending_mailcow_cc_username'])) {
|
if (!isset($_SESSION['pending_mailcow_cc_username'])) {
|
||||||
switch ($category) {
|
switch ($category) {
|
||||||
|
@ -1231,7 +1357,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username'])) {
|
if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username']) || !isset($_SESSION["mailcow_cc_username"])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(array(
|
echo json_encode(array(
|
||||||
'type' => 'error',
|
'type' => 'error',
|
||||||
|
@ -1365,7 +1491,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "edit":
|
case "edit":
|
||||||
if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username'])) {
|
if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username']) || !isset($_SESSION["mailcow_cc_username"])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(array(
|
echo json_encode(array(
|
||||||
'type' => 'error',
|
'type' => 'error',
|
||||||
|
@ -1435,6 +1561,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
case "rspamd-map":
|
case "rspamd-map":
|
||||||
process_edit_return(rspamd('edit', array_merge(array('map' => $items), $attr)));
|
process_edit_return(rspamd('edit', array_merge(array('map' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
|
case "fido2-fn":
|
||||||
|
process_edit_return(fido2(array('action' => 'edit_fn', 'fido2_attrs' => $attr)));
|
||||||
|
break;
|
||||||
case "app_links":
|
case "app_links":
|
||||||
process_edit_return(customize('edit', 'app_links', $attr));
|
process_edit_return(customize('edit', 'app_links', $attr));
|
||||||
break;
|
break;
|
||||||
|
@ -1552,4 +1681,3 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
|
||||||
unset($_SESSION['return']);
|
unset($_SESSION['return']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -425,6 +425,7 @@
|
||||||
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
|
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
|
||||||
"transport_dest_exists": "Transport Maps Ziel \"%s\" existiert bereits",
|
"transport_dest_exists": "Transport Maps Ziel \"%s\" existiert bereits",
|
||||||
"u2f_verification_failed": "U2F-Verifizierung fehlgeschlagen: %s",
|
"u2f_verification_failed": "U2F-Verifizierung fehlgeschlagen: %s",
|
||||||
|
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
|
||||||
"unknown": "Ein unbekannter Fehler trat auf",
|
"unknown": "Ein unbekannter Fehler trat auf",
|
||||||
"unknown_tfa_method": "Unbekannte TFA Methode",
|
"unknown_tfa_method": "Unbekannte TFA Methode",
|
||||||
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
|
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
|
||||||
|
@ -598,8 +599,10 @@
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
|
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
|
||||||
|
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
|
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
|
||||||
|
"other_logins": "Key Login",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"username": "Benutzername"
|
"username": "Benutzername"
|
||||||
},
|
},
|
||||||
|
@ -880,6 +883,7 @@
|
||||||
"upload_success": "Datei wurde erfolgreich hochgeladen",
|
"upload_success": "Datei wurde erfolgreich hochgeladen",
|
||||||
"verified_totp_login": "TOTP-Anmeldung verifiziert",
|
"verified_totp_login": "TOTP-Anmeldung verifiziert",
|
||||||
"verified_u2f_login": "U2F-Anmeldung verifiziert",
|
"verified_u2f_login": "U2F-Anmeldung verifiziert",
|
||||||
|
"verified_fido2_login": "FIDO2-Anmeldung verifiziert",
|
||||||
"verified_yotp_login": "Yubico OTP-Anmeldung verifiziert"
|
"verified_yotp_login": "Yubico OTP-Anmeldung verifiziert"
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
|
@ -902,10 +906,24 @@
|
||||||
"tfa": "Zwei-Faktor-Authentifizierung",
|
"tfa": "Zwei-Faktor-Authentifizierung",
|
||||||
"totp": "Time-based OTP (Google Authenticator etc.)",
|
"totp": "Time-based OTP (Google Authenticator etc.)",
|
||||||
"u2f": "U2F-Authentifizierung",
|
"u2f": "U2F-Authentifizierung",
|
||||||
"waiting_usb_auth": "<i>Warte auf USB-Gerät...</i><br><br>Bitte jetzt den vorgesehenen Taster des U2F-USB-Gerätes berühren.",
|
"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 U2F 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"
|
"yubi_otp": "Yubico OTP-Authentifizierung"
|
||||||
},
|
},
|
||||||
|
"fido2": {
|
||||||
|
"set_fn": "Benutzerfreundlichen Namen konfigurieren",
|
||||||
|
"fn": "Benutzerfreundlicher Name",
|
||||||
|
"rename": "umbenennen",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"register_status": "Registrierungsstatus",
|
||||||
|
"known_ids": "Bekannte IDs",
|
||||||
|
"none": "Deaktiviert",
|
||||||
|
"set_fido2": "Registriere FIDO2 Gerät",
|
||||||
|
"start_fido2_validation": "Starte FIDO2 Validierung",
|
||||||
|
"fido2_auth": "Anmeldung über FIDO2",
|
||||||
|
"fido2_success": "Das Gerät wurde erfolgreich registriert",
|
||||||
|
"fido2_validation_failed": "Validierung fehlgeschlagen"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Aktion",
|
"action": "Aktion",
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
|
|
|
@ -425,6 +425,7 @@
|
||||||
"totp_verification_failed": "TOTP verification failed",
|
"totp_verification_failed": "TOTP verification failed",
|
||||||
"transport_dest_exists": "Transport destination \"%s\" exists",
|
"transport_dest_exists": "Transport destination \"%s\" exists",
|
||||||
"u2f_verification_failed": "U2F verification failed: %s",
|
"u2f_verification_failed": "U2F verification failed: %s",
|
||||||
|
"fido2_verification_failed": "U2F verification failed: %s",
|
||||||
"unknown": "An unknown error occurred",
|
"unknown": "An unknown error occurred",
|
||||||
"unknown_tfa_method": "Unknown TFA method",
|
"unknown_tfa_method": "Unknown TFA method",
|
||||||
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
|
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
|
||||||
|
@ -598,8 +599,10 @@
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"delayed": "Login was delayed by %s seconds.",
|
"delayed": "Login was delayed by %s seconds.",
|
||||||
|
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
|
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
|
||||||
|
"other_logins": "Key login",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
},
|
},
|
||||||
|
@ -880,6 +883,7 @@
|
||||||
"upload_success": "File uploaded successfully",
|
"upload_success": "File uploaded successfully",
|
||||||
"verified_totp_login": "Verified TOTP login",
|
"verified_totp_login": "Verified TOTP login",
|
||||||
"verified_u2f_login": "Verified U2F login",
|
"verified_u2f_login": "Verified U2F login",
|
||||||
|
"verified_fido2_login": "Verified FIDO2 login",
|
||||||
"verified_yotp_login": "Verified Yubico OTP login"
|
"verified_yotp_login": "Verified Yubico OTP login"
|
||||||
},
|
},
|
||||||
"tfa": {
|
"tfa": {
|
||||||
|
@ -902,10 +906,24 @@
|
||||||
"tfa": "Two-factor authentication",
|
"tfa": "Two-factor authentication",
|
||||||
"totp": "Time-based OTP (Google Authenticator, Authy, etc.)",
|
"totp": "Time-based OTP (Google Authenticator, Authy, etc.)",
|
||||||
"u2f": "U2F authentication",
|
"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_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 U2F registration by tapping the button on your U2F USB device.",
|
"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"
|
"yubi_otp": "Yubico OTP authentication"
|
||||||
},
|
},
|
||||||
|
"fido2": {
|
||||||
|
"set_fn": "Set friendly name",
|
||||||
|
"fn": "Friendly name",
|
||||||
|
"rename": "rename",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"register_status": "Registration status",
|
||||||
|
"known_ids": "Known IDs",
|
||||||
|
"none": "Disabled",
|
||||||
|
"set_fido2": "Register FIDO2 device",
|
||||||
|
"start_fido2_validation": "Start FIDO2 validation",
|
||||||
|
"fido2_auth": "Login with FIDO2",
|
||||||
|
"fido2_success": "Device successfully registered",
|
||||||
|
"fido2_validation_failed": "Validation failed"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
|
|
@ -104,6 +104,34 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- add domain admin modal -->
|
</div><!-- add domain admin modal -->
|
||||||
|
<!-- change fido2 fn -->
|
||||||
|
<div class="modal fade" id="fido2ChangeFn" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<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['fido2']['set_fn'];?></h3>
|
||||||
|
<p class="help-block" id="fido2_subject_desc" data-fido2-subject=""></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="form-horizontal" data-cached-form="false" data-id="fido2ChangeFn" role="form" method="post" autocomplete="off">
|
||||||
|
<input type="hidden" class="form-control" name="fido2_subject" id="fido2_subject">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-4" for="fido2_fn"><?=$lang['fido2']['fn'];?>:</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="text" class="form-control" name="fido2_fn">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-4 col-sm-8">
|
||||||
|
<button class="btn btn-default" data-action="edit_selected" data-id="fido2ChangeFn" data-item="null" data-api-url='edit/fido2-fn' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- add domain admin modal -->
|
||||||
<!-- add oauth2 client modal -->
|
<!-- add oauth2 client modal -->
|
||||||
<div class="modal fade" id="addOAuth2ClientModal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal fade" id="addOAuth2ClientModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
|
|
|
@ -4,6 +4,34 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
<!-- change fido2 fn -->
|
||||||
|
<div class="modal fade" id="fido2ChangeFn" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<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['fido2']['set_fn'];?></h3>
|
||||||
|
<p class="help-block" id="fido2_subject_desc" data-fido2-subject=""></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="form-horizontal" data-cached-form="false" data-id="fido2ChangeFn" role="form" method="post" autocomplete="off">
|
||||||
|
<input type="hidden" class="form-control" name="fido2_subject" id="fido2_subject">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-4" for="fido2_fn"><?=$lang['fido2']['fn'];?>:</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="text" class="form-control" name="fido2_fn">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-4 col-sm-8">
|
||||||
|
<button class="btn btn-default" data-action="edit_selected" data-id="fido2ChangeFn" data-item="null" data-api-url='edit/fido2-fn' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- add domain admin modal -->
|
||||||
<!-- add sync job modal -->
|
<!-- add sync job modal -->
|
||||||
<div class="modal fade" id="addSyncJobModal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal fade" id="addSyncJobModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
|
|
|
@ -9,10 +9,12 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
$tfa_data = get_tfa();
|
$tfa_data = get_tfa();
|
||||||
|
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<h3><?=$lang['user']['user_settings'];?></h3>
|
<h3><?=$lang['user']['user_settings'];?></h3>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><?=$lang['user']['user_settings'];?></div>
|
<div class="panel-heading"><?=$lang['user']['user_settings'];?></div>
|
||||||
|
@ -33,6 +35,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<? // TFA ?>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?></div>
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?></div>
|
||||||
<div class="col-sm-9 col-xs-7">
|
<div class="col-sm-9 col-xs-7">
|
||||||
|
@ -61,6 +65,58 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<? // FIDO2 ?>
|
||||||
|
<legend style="margin-top:20px">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="margin-bottom: -5px;">
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
<?=$lang['fido2']['fido2_auth'];?></legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['fido2']['known_ids'];?>:</div>
|
||||||
|
<div class="col-sm-9 col-xs-7">
|
||||||
|
<div id="tfa_additional">
|
||||||
|
<?php
|
||||||
|
if (!empty($fido2_data)) {
|
||||||
|
foreach ($fido2_data as $key_info) {
|
||||||
|
?>
|
||||||
|
<form style="display:inline;" method="post">
|
||||||
|
<input type="hidden" name="unset_fido2_key" value="<?=$key_info['subject'];?>" />
|
||||||
|
<div data-toggle="tooltip" data-placement="top" title="<?=$key_info['subject'];?>" class="label label-keys label-<?=($_SESSION['fido2_subject'] == $key_info['subject']) ? 'success' : 'default'; ?>">
|
||||||
|
<?=(!empty($key_info['fn']))?$key_info['fn']:$key_info['subject'];?>
|
||||||
|
<a href="#" class="key-action" onClick='return confirm("<?=$lang['admin']['ays'];?>")?$(this).closest("form").submit():"";'>
|
||||||
|
[<?=strtolower($lang['admin']['remove']);?>]
|
||||||
|
</a>
|
||||||
|
<a href="#" class="key-action" data-subject="<?=base64_encode($key_info['subject']);?>" data-toggle="modal" data-target="#fido2ChangeFn">
|
||||||
|
[<?=strtolower($lang['fido2']['rename']);?>]
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo "-";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
|
<button class="btn btn-sm btn-primary" id="register-fido2"><?=$lang['fido2']['set_fido2'];?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row" id="status-fido2">
|
||||||
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['fido2']['register_status'];?>:</div>
|
||||||
|
<div class="col-sm-9 col-xs-7">
|
||||||
|
<div id="fido2-alerts">-</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue