276 lines
9.3 KiB
PHP
276 lines
9.3 KiB
PHP
|
<?php
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
/**
|
||
|
* @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
|
||
|
*
|
||
|
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||
|
* @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\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(
|
||
|
null,
|
||
|
false,
|
||
|
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
|
||
|
);
|
||
|
|
||
|
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
|
||
|
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
|
||
|
);
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
}
|