123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- <?php
- declare(strict_types=1);
- /**
- * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- namespace OC\Authentication\WebAuthn;
- use Cose\Algorithm\Signature\ECDSA\ES256;
- use Cose\Algorithm\Signature\RSA\RS256;
- use Cose\Algorithms;
- use GuzzleHttp\Psr7\ServerRequest;
- use OC\Authentication\WebAuthn\Db\PublicKeyCredentialEntity;
- use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper;
- use OCP\AppFramework\Db\DoesNotExistException;
- use OCP\IConfig;
- use OCP\IUser;
- use Psr\Log\LoggerInterface;
- use Webauthn\AttestationStatement\AttestationObjectLoader;
- use Webauthn\AttestationStatement\AttestationStatementSupportManager;
- use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
- use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
- use Webauthn\AuthenticatorAssertionResponse;
- use Webauthn\AuthenticatorAssertionResponseValidator;
- use Webauthn\AuthenticatorAttestationResponse;
- use Webauthn\AuthenticatorAttestationResponseValidator;
- use Webauthn\AuthenticatorSelectionCriteria;
- use Webauthn\PublicKeyCredentialCreationOptions;
- use Webauthn\PublicKeyCredentialDescriptor;
- use Webauthn\PublicKeyCredentialLoader;
- use Webauthn\PublicKeyCredentialParameters;
- use Webauthn\PublicKeyCredentialRequestOptions;
- use Webauthn\PublicKeyCredentialRpEntity;
- use Webauthn\PublicKeyCredentialUserEntity;
- use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
- class Manager {
- /** @var CredentialRepository */
- private $repository;
- /** @var PublicKeyCredentialMapper */
- private $credentialMapper;
- /** @var LoggerInterface */
- private $logger;
- /** @var IConfig */
- private $config;
- public function __construct(
- CredentialRepository $repository,
- PublicKeyCredentialMapper $credentialMapper,
- LoggerInterface $logger,
- IConfig $config,
- ) {
- $this->repository = $repository;
- $this->credentialMapper = $credentialMapper;
- $this->logger = $logger;
- $this->config = $config;
- }
- public function startRegistration(IUser $user, string $serverHost): PublicKeyCredentialCreationOptions {
- $rpEntity = new PublicKeyCredentialRpEntity(
- 'Nextcloud', //Name
- $this->stripPort($serverHost), //ID
- null //Icon
- );
- $userEntity = new PublicKeyCredentialUserEntity(
- $user->getUID(), // Name
- $user->getUID(), // ID
- $user->getDisplayName() // Display name
- // 'https://foo.example.co/avatar/123e4567-e89b-12d3-a456-426655440000' //Icon
- );
- $challenge = random_bytes(32);
- $publicKeyCredentialParametersList = [
- new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256),
- new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_RS256),
- ];
- $timeout = 60000;
- $excludedPublicKeyDescriptors = [
- ];
- $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
- AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
- AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
- null,
- false,
- );
- return new PublicKeyCredentialCreationOptions(
- $rpEntity,
- $userEntity,
- $challenge,
- $publicKeyCredentialParametersList,
- $authenticatorSelectionCriteria,
- PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
- $excludedPublicKeyDescriptors,
- $timeout,
- );
- }
- public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, string $name, string $data): PublicKeyCredentialEntity {
- $tokenBindingHandler = new TokenBindingNotSupportedHandler();
- $attestationStatementSupportManager = new AttestationStatementSupportManager();
- $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
- $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
- $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
- // Extension Output Checker Handler
- $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
- // Authenticator Attestation Response Validator
- $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
- $attestationStatementSupportManager,
- $this->repository,
- $tokenBindingHandler,
- $extensionOutputCheckerHandler
- );
- $authenticatorAttestationResponseValidator->setLogger($this->logger);
- try {
- // Load the data
- $publicKeyCredential = $publicKeyCredentialLoader->load($data);
- $response = $publicKeyCredential->response;
- // Check if the response is an Authenticator Attestation Response
- if (!$response instanceof AuthenticatorAttestationResponse) {
- throw new \RuntimeException('Not an authenticator attestation response');
- }
- // Check the response against the request
- $request = ServerRequest::fromGlobals();
- $publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
- $response,
- $publicKeyCredentialCreationOptions,
- $request,
- ['localhost'],
- );
- } catch (\Throwable $exception) {
- throw $exception;
- }
- // Persist the data
- $userVerification = $response->attestationObject->authData->isUserVerified();
- return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification);
- }
- private function stripPort(string $serverHost): string {
- return preg_replace('/(:\d+$)/', '', $serverHost);
- }
- public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
- // List of registered PublicKeyCredentialDescriptor classes associated to the user
- $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,
- $credential->publicKeyCredentialId,
- );
- }, $this->credentialMapper->findAllForUid($uid));
- // Public Key Credential Request Options
- return new PublicKeyCredentialRequestOptions(
- random_bytes(32), // Challenge
- $this->stripPort($serverHost), // Relying Party ID
- $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
- $userVerificationRequirement,
- 60000, // Timeout
- );
- }
- public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, string $data, string $uid) {
- $attestationStatementSupportManager = new AttestationStatementSupportManager();
- $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
- $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
- $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
- $tokenBindingHandler = new TokenBindingNotSupportedHandler();
- $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
- $algorithmManager = new \Cose\Algorithm\Manager();
- $algorithmManager->add(new ES256());
- $algorithmManager->add(new RS256());
- $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
- $this->repository,
- $tokenBindingHandler,
- $extensionOutputCheckerHandler,
- $algorithmManager,
- );
- $authenticatorAssertionResponseValidator->setLogger($this->logger);
- try {
- $this->logger->debug('Loading publickey credentials from: ' . $data);
- // Load the data
- $publicKeyCredential = $publicKeyCredentialLoader->load($data);
- $response = $publicKeyCredential->response;
- // Check if the response is an Authenticator Attestation Response
- if (!$response instanceof AuthenticatorAssertionResponse) {
- throw new \RuntimeException('Not an authenticator attestation response');
- }
- // Check the response against the request
- $request = ServerRequest::fromGlobals();
- $publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
- $publicKeyCredential->rawId,
- $response,
- $publicKeyCredentialRequestOptions,
- $request,
- $uid,
- ['localhost'],
- );
- } catch (\Throwable $e) {
- throw $e;
- }
- return true;
- }
- public function deleteRegistration(IUser $user, int $id): void {
- try {
- $entry = $this->credentialMapper->findById($user->getUID(), $id);
- } catch (DoesNotExistException $e) {
- $this->logger->warning("WebAuthn device $id does not exist, can't delete it");
- return;
- }
- $this->credentialMapper->delete($entry);
- }
- public function isWebAuthnAvailable(): bool {
- if (!extension_loaded('bcmath')) {
- return false;
- }
- if (!extension_loaded('gmp')) {
- return false;
- }
- if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) {
- return false;
- }
- return true;
- }
- }
|