webauthn: store registrations, check for duplicate security keys

This commit is contained in:
Roland Gruber 2019-12-01 18:11:19 +01:00
parent 0f13e3c8ba
commit 2aabad9a3d
4 changed files with 153 additions and 10 deletions

View File

@ -3,3 +3,4 @@ config.cfg
/serverCerts.pem
/pdf/
/profiles/
*.sqlite

View File

@ -523,6 +523,19 @@ class WebauthnProvider extends BaseProvider {
* @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput()
*/
public function addCustomInput(&$row, $userDn) {
if (version_compare(phpversion(), '7.2.0') < 0) {
$row->add(new htmlStatusMessage('ERROR', 'Webauthn requires PHP 7.2.'), 12);
return;
}
if (!extension_loaded('PDO')) {
$row->add(new htmlStatusMessage('ERROR', 'Webauthn requires the PDO extension for PHP.'), 12);
return;
}
$pdoDrivers = \PDO::getAvailableDrivers();
if (!in_array('sqlite', $pdoDrivers)) {
$row->add(new htmlStatusMessage('ERROR', 'Webauthn requires the sqlite PDO driver for PHP.'), 12);
return;
}
$pathPrefix = $this->config->isSelfService ? '../' : '';
$row->add(new htmlImage($pathPrefix . '../graphics/webauthn.svg'), 12);
$row->addVerticalSpacer('1rem');

View File

@ -16,7 +16,7 @@ use Cose\Algorithm\Signature\RSA\RS384;
use Cose\Algorithm\Signature\RSA\RS512;
use \Cose\Algorithms;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use PDO;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Request;
use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport;
@ -30,6 +30,7 @@ use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use \Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialLoader;
use \Webauthn\PublicKeyCredentialRpEntity;
use \Webauthn\PublicKeyCredentialParameters;
@ -91,6 +92,7 @@ function getRegistrationObject($dn, $isSelfService) {
$userEntity = getUserEntity($dn);
$challenge = generateRandomPassword(32);
$credentialParameters = getCredentialParameters();
$excludedKeys = getExcludedKeys($userEntity);
$timeout = 20000;
$registrationObject = new PublicKeyCredentialCreationOptions(
$rpEntity,
@ -98,7 +100,7 @@ function getRegistrationObject($dn, $isSelfService) {
$challenge,
$credentialParameters,
$timeout,
array(),
$excludedKeys,
new AuthenticatorSelectionCriteria(),
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
new AuthenticationExtensionsClientInputs());
@ -152,6 +154,22 @@ function getCredentialParameters() {
);
}
/**
* Returns a list of all credential ids that are already registered.
*
* @param PublicKeyCredentialUserEntity $user user data
* @return PublicKeyCredentialDescriptor[] credential ids
*/
function getExcludedKeys($user) {
$keys = array();
$repository = new PublicKeyCredentialSourceRepositorySQLite();
$credentialSources = $repository->findAllForUserEntity($user);
foreach ($credentialSources as $credentialSource) {
$keys[] = new PublicKeyCredentialDescriptor(PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $credentialSource->getPublicKeyCredentialId());
}
return $keys;
}
/**
* Verifies the registration and stores it in the database.
*
@ -263,6 +281,8 @@ function getExtensionOutputChecker() {
*/
class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSourceRepository {
const TABLE_NAME = 'lam_webauthn';
/**
* Finds the public key for the given credential id.
*
@ -270,8 +290,19 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
* @return PublicKeyCredentialSource|null credential source
*/
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource {
// TODO: Implement findOneByCredentialId() method.
logNewMessage(LOG_WARNING, 'FIND ONE: ' . $publicKeyCredentialId);
try {
$pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . self::TABLE_NAME . ' where credentialId = :credentialid');
$statement->execute(array(':credentialid' => base64_encode($publicKeyCredentialId)));
$results = $statement->fetchAll();
if (!empty($results)) {
$jsonArray = json_decode($results[0]['credentialSource'], true);
return PublicKeyCredentialSource::createFromArray($jsonArray);
}
}
catch (\PDOException $e) {
logNewMessage(LOG_ERR, 'Webauthn database error: ' . $e->getMessage());
}
return null;
}
@ -282,9 +313,21 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
* @return PublicKeyCredentialSource[] credential sources
*/
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
// TODO: Implement findAllForUserEntity() method.
logNewMessage(LOG_WARNING, 'FIND ALL: ' . json_encode($publicKeyCredentialUserEntity));
return array();
$credentials = array();
try {
$pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . self::TABLE_NAME . ' where userId = :userid');
$statement->execute(array(':userid' => $publicKeyCredentialUserEntity->getId()));
$results = $statement->fetchAll();
foreach ($results as $result) {
$jsonArray = json_decode($results[0]['credentialSource'], true);
$credentials[] = PublicKeyCredentialSource::createFromArray($jsonArray);
}
}
catch (\PDOException $e) {
logNewMessage(LOG_ERR, 'Webauthn database error: ' . $e->getMessage());
}
return $credentials;
}
/**
@ -293,8 +336,83 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
* @param PublicKeyCredentialSource $publicKeyCredentialSource credential
*/
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void {
// TODO: Implement saveCredentialSource() method.
logNewMessage(LOG_WARNING, 'SAVE: ' . json_encode($publicKeyCredentialSource));
$json = json_encode($publicKeyCredentialSource);
$credentialId = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
$userId = $publicKeyCredentialSource->getUserHandle();
$registrationTime = time();
$pdo = $this->getPDO();
$statement = $pdo->prepare('insert into ' . self::TABLE_NAME . ' (userId, credentialId, credentialSource, registrationTime) VALUES (?, ?, ?, ?)');
$statement->execute(array(
$userId,
$credentialId,
$json,
$registrationTime
));
logNewMessage(LOG_DEBUG, 'Stored new credential for ' . $userId);
}
/**
* Returns the database URL.
*
* @return string the PDO URL
*/
protected function getPdoUrl() {
$fileName = dirname(__FILE__) . '/../config/__lam.webauthn.sqlite';
if (!file_exists($fileName)) {
$handle = fopen($fileName, 'w');
fclose($handle);
chmod($fileName, 0600);
}
return 'sqlite:' . $fileName;
}
/**
* Returns the PDO.
*
* @return PDO PDO
*/
protected function getPDO() {
$pdo = new PDO($this->getPdoUrl(), null, null, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
));
// initial schema
if (!$this->tableExists($pdo, self::TABLE_NAME)) {
$this->createInitialSchema($pdo);
}
return $pdo;
}
/**
* Checks if the given table name exists.
*
* @param PDO $pdo PDO object
* @param String $tableName table name to check
* @return boolean table exists
*/
protected function tableExists(&$pdo, $tableName) {
try {
$result = $pdo->query("SELECT 1 FROM $tableName LIMIT 1");
return ($result === false) ? false : true;
} catch (\PDOException $e) {
return false;
}
}
/**
* Creates the initial schema.
*
* @param PDO $pdo PDO object
*/
protected function createInitialSchema($pdo) {
logNewMessage(LOG_DEBUG, 'Creating database table ' . self::TABLE_NAME);
$sql = 'create table ' . self::TABLE_NAME . '('
. 'userId TEXT NOT NULL,'
. 'credentialId TEXT NOT NULL,'
. 'credentialSource TEXT NOT NULL,'
. 'registrationTime VARCHAR(11) NOT NULL,'
. 'PRIMARY KEY(userId,credentialId)'
. ');';
$pdo->exec($sql);
}
}

View File

@ -1412,6 +1412,16 @@ window.lam.webauthn.run = function(prefix) {
window.lam.webauthn.register = function(publicKey) {
publicKey.challenge = Uint8Array.from(window.atob(publicKey.challenge), c=>c.charCodeAt(0));
publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), c=>c.charCodeAt(0));
publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon;
if (publicKey.excludeCredentials) {
for (var i = 0; i < publicKey.excludeCredentials.length; i++) {
let idOrig = publicKey.excludeCredentials[i]['id'];
idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/");
let idOrigDecoded = atob(idOrig);
let idArray = Uint8Array.from(idOrigDecoded, c => c.charCodeAt(0))
publicKey.excludeCredentials[i]['id'] = idArray;
}
}
navigator.credentials.create({publicKey})
.then(function (data) {
let publicKeyCredential = {
@ -1428,7 +1438,8 @@ window.lam.webauthn.register = function(publicKey) {
form.append('<input type="hidden" name="sig_response" value="' + response + '"/>');
form.submit();
}, function (error) {
console.log(error);
console.log(error.message);
jQuery('#btn_logout').click();
});
}