feat(webauthn): Add user verification to webauthn challenges

Require user verification if all tokens are registered
with UV flag, else discourage it

Signed-off-by: S1m <git@sgougeon.fr>
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
This commit is contained in:
S1m 2024-03-24 12:17:10 +01:00 коммит произвёл Joas Schilling
Родитель e218d1f98e
Коммит 9189bc290b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 74434EFE0D2E2205
7 изменённых файлов: 57 добавлений и 9 удалений

Просмотреть файл

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 S1m <git@sgougeon.fr>
* SPDX-FileCopyrightText: 2024 Richard Steinmetz <richard@steinmetz.cloud>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version30000Date20240815080800 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('webauthn');
$table->addColumn('user_verification', Types::BOOLEAN, ['notnull' => false, 'default' => false]);
return $schema;
}
}

Просмотреть файл

@ -1366,6 +1366,7 @@ return array(
'OC\\Core\\Migrations\\Version30000Date20240708160048' => $baseDir . '/core/Migrations/Version30000Date20240708160048.php',
'OC\\Core\\Migrations\\Version30000Date20240717111406' => $baseDir . '/core/Migrations/Version30000Date20240717111406.php',
'OC\\Core\\Migrations\\Version30000Date20240814180800' => $baseDir . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',

Просмотреть файл

@ -1399,6 +1399,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version30000Date20240708160048' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240708160048.php',
'OC\\Core\\Migrations\\Version30000Date20240717111406' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240717111406.php',
'OC\\Core\\Migrations\\Version30000Date20240814180800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',

Просмотреть файл

@ -44,7 +44,7 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository {
}, $entities);
}
public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity {
public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity {
$oldEntity = null;
try {
@ -58,13 +58,18 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository {
$name = 'default';
}
$entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource);
$entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification);
if ($oldEntity) {
$entity->setId($oldEntity->getId());
if ($defaultName) {
$entity->setName($oldEntity->getName());
}
// Don't downgrade UV just because it was skipped during a login due to another key
if ($oldEntity->getUserVerification()) {
$entity->setUserVerification(true);
}
}
return $this->credentialMapper->insertOrUpdate($entity);

Просмотреть файл

@ -23,6 +23,10 @@ use Webauthn\PublicKeyCredentialSource;
* @method void setPublicKeyCredentialId(string $id);
* @method string getData();
* @method void setData(string $data);
*
* @since 30.0.0 Add userVerification attribute
* @method bool|null getUserVerification();
* @method void setUserVerification(bool $userVerification);
*/
class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
/** @var string */
@ -37,20 +41,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
/** @var string */
protected $data;
/** @var bool|null */
protected $userVerification;
public function __construct() {
$this->addType('name', 'string');
$this->addType('uid', 'string');
$this->addType('publicKeyCredentialId', 'string');
$this->addType('data', 'string');
$this->addType('userVerification', 'boolean');
}
public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity {
public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity {
$publicKeyCredentialEntity = new self();
$publicKeyCredentialEntity->setName($name);
$publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle());
$publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()));
$publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource));
$publicKeyCredentialEntity->setUserVerification($userVerification);
return $publicKeyCredentialEntity;
}

Просмотреть файл

@ -88,8 +88,8 @@ class Manager {
];
$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
null,
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
null,
false,
);
@ -151,7 +151,8 @@ class Manager {
}
// Persist the data
return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
$userVerification = $response->attestationObject->authData->isUserVerified();
return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification);
}
private function stripPort(string $serverHost): string {
@ -160,7 +161,11 @@ class Manager {
public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
// List of registered PublicKeyCredentialDescriptor classes associated to the user
$registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
$userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
$registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) {
if ($entity->getUserVerification() !== true) {
$userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
}
$credential = $entity->toPublicKeyCredentialSource();
return new PublicKeyCredentialDescriptor(
$credential->type,
@ -173,7 +178,7 @@ class Manager {
random_bytes(32), // Challenge
$this->stripPort($serverHost), // Relying Party ID
$registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
$userVerificationRequirement,
60000, // Timeout
);
}

Просмотреть файл

@ -9,7 +9,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number.
$OC_Version = [31, 0, 0, 0];
$OC_Version = [31, 0, 0, 1];
// The human-readable string
$OC_VersionString = '31.0.0 dev';