From 2aabad9a3d29599acf063c46106a6950755116cd Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sun, 1 Dec 2019 18:11:19 +0100 Subject: [PATCH] webauthn: store registrations, check for duplicate security keys --- lam/config/.gitignore | 1 + lam/lib/2factor.inc | 13 ++++ lam/lib/webauthn.inc | 136 ++++++++++++++++++++++++++++++++--- lam/templates/lib/500_lam.js | 13 +++- 4 files changed, 153 insertions(+), 10 deletions(-) diff --git a/lam/config/.gitignore b/lam/config/.gitignore index b6d3e26b..aad238c3 100644 --- a/lam/config/.gitignore +++ b/lam/config/.gitignore @@ -3,3 +3,4 @@ config.cfg /serverCerts.pem /pdf/ /profiles/ +*.sqlite \ No newline at end of file diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc index 677e1ef9..038e35df 100644 --- a/lam/lib/2factor.inc +++ b/lam/lib/2factor.inc @@ -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'); diff --git a/lam/lib/webauthn.inc b/lam/lib/webauthn.inc index 8dd67fec..b0847854 100644 --- a/lam/lib/webauthn.inc +++ b/lam/lib/webauthn.inc @@ -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); } } diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index cd2b850b..b51c55ed 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -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(''); form.submit(); }, function (error) { - console.log(error); + console.log(error.message); + jQuery('#btn_logout').click(); }); }