summaryrefslogtreecommitdiffstats
path: root/lib/private/Authentication/WebAuthn/Manager.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Authentication/WebAuthn/Manager.php')
-rw-r--r--lib/private/Authentication/WebAuthn/Manager.php269
1 files changed, 269 insertions, 0 deletions
diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php
new file mode 100644
index 00000000000..32a90345b5c
--- /dev/null
+++ b/lib/private/Authentication/WebAuthn/Manager.php
@@ -0,0 +1,269 @@
+<?php
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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\ILogger;
+use OCP\IUser;
+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\PublicKeyCredentialSource;
+use Webauthn\PublicKeyCredentialUserEntity;
+use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
+
+class Manager {
+
+ /** @var CredentialRepository */
+ private $repository;
+
+ /** @var PublicKeyCredentialMapper */
+ private $credentialMapper;
+
+ /** @var ILogger */
+ private $logger;
+
+ /** @var IConfig */
+ private $config;
+
+ public function __construct(
+ CredentialRepository $repository,
+ PublicKeyCredentialMapper $credentialMapper,
+ ILogger $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();
+
+ return new PublicKeyCredentialCreationOptions(
+ $rpEntity,
+ $userEntity,
+ $challenge,
+ $publicKeyCredentialParametersList,
+ $timeout,
+ $excludedPublicKeyDescriptors,
+ $authenticatorSelectionCriteria,
+ PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
+ null
+ );
+ }
+
+ 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
+ );
+
+ try {
+ // Load the data
+ $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $response = $publicKeyCredential->getResponse();
+
+ // 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);
+ } catch (\Throwable $exception) {
+ throw $exception;
+ }
+
+ // Persist the data
+ return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
+ }
+
+ 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
+ $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
+ $credential = $entity->toPublicKeyCredentialSource();
+ return new PublicKeyCredentialDescriptor(
+ $credential->getType(),
+ $credential->getPublicKeyCredentialId()
+ );
+ }, $this->credentialMapper->findAllForUid($uid));
+
+ // Public Key Credential Request Options
+ return new PublicKeyCredentialRequestOptions(
+ random_bytes(32), // Challenge
+ 60000, // Timeout
+ $this->stripPort($serverHost), // Relying Party ID
+ $registeredPublicKeyCredentialDescriptors // Registered PublicKeyCredentialDescriptor classes
+ );
+ }
+
+ 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
+ );
+
+ try {
+ $this->logger->debug('Loading publickey credentials from: ' . $data);
+
+ // Load the data
+ $publicKeyCredential = $publicKeyCredentialLoader->load($data);
+ $response = $publicKeyCredential->getResponse();
+
+ // 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->getRawId(),
+ $response,
+ $publicKeyCredentialRequestOptions,
+ $request,
+ $uid
+ );
+
+ } 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;
+ }
+}