[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/multifactorauth/webauthn/src/Hotfix/ -> Server.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  Multifactorauth.webauthn
   6   *
   7   * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9   * @copyright   (C) 2014-2019 Spomky-Labs
  10   * @license     This software may be modified and distributed under the terms
  11   *              of the MIT license.
  12   *              See libraries/vendor/web-auth/webauthn-lib/LICENSE
  13   */
  14  
  15  namespace Joomla\Plugin\Multifactorauth\Webauthn\Hotfix;
  16  
  17  use Assert\Assertion;
  18  use Cose\Algorithm\Algorithm;
  19  use Cose\Algorithm\ManagerFactory;
  20  use Cose\Algorithm\Signature\ECDSA;
  21  use Cose\Algorithm\Signature\EdDSA;
  22  use Cose\Algorithm\Signature\RSA;
  23  use Psr\Http\Client\ClientInterface;
  24  use Psr\Http\Message\RequestFactoryInterface;
  25  use Psr\Http\Message\ServerRequestInterface;
  26  use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport;
  27  use Webauthn\AttestationStatement\AttestationObjectLoader;
  28  use Webauthn\AttestationStatement\AttestationStatementSupportManager;
  29  use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
  30  use Webauthn\AttestationStatement\PackedAttestationStatementSupport;
  31  use Webauthn\AttestationStatement\TPMAttestationStatementSupport;
  32  use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
  33  use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
  34  use Webauthn\AuthenticatorAssertionResponse;
  35  use Webauthn\AuthenticatorAssertionResponseValidator;
  36  use Webauthn\AuthenticatorAttestationResponse;
  37  use Webauthn\AuthenticatorAttestationResponseValidator;
  38  use Webauthn\AuthenticatorSelectionCriteria;
  39  use Webauthn\MetadataService\MetadataStatementRepository;
  40  use Webauthn\PublicKeyCredentialCreationOptions;
  41  use Webauthn\PublicKeyCredentialDescriptor;
  42  use Webauthn\PublicKeyCredentialLoader;
  43  use Webauthn\PublicKeyCredentialParameters;
  44  use Webauthn\PublicKeyCredentialRequestOptions;
  45  use Webauthn\PublicKeyCredentialRpEntity;
  46  use Webauthn\PublicKeyCredentialSource;
  47  use Webauthn\PublicKeyCredentialSourceRepository;
  48  use Webauthn\PublicKeyCredentialUserEntity;
  49  use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
  50  
  51  // phpcs:disable PSR1.Files.SideEffects
  52  \defined('_JEXEC') or die;
  53  // phpcs:enable PSR1.Files.SideEffects
  54  
  55  /**
  56   * Customised WebAuthn server object.
  57   *
  58   * We had to fork the server object from the WebAuthn server package to address an issue with PHP 8.
  59   *
  60   * We are currently using an older version of the WebAuthn library (2.x) which was written before
  61   * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of
  62   * Joomla's Semantic Versioning promise.
  63   *
  64   * The FidoU2FAttestationStatementSupport and AndroidKeyAttestationStatementSupport classes force
  65   * an assertion on the result of the openssl_pkey_get_public() function, assuming it will return a
  66   * resource. However, starting with PHP 8.0 this function returns an OpenSSLAsymmetricKey object
  67   * and the assertion fails. As a result, you cannot use Android or FIDO U2F keys with WebAuthn.
  68   *
  69   * The assertion check is in a private method, therefore we have to fork both attestation support
  70   * classes to change the assertion. The assertion takes place through a third party library we
  71   * cannot (and should not!) modify.
  72   *
  73   * The assertions objects, however, are injected to the attestation support manager in a private
  74   * method of the Server object. Because literally everything in this class is private we have no
  75   * option than to fork the entire class to apply our two forked attestation support classes.
  76   *
  77   * This is marked as deprecated because we'll be able to upgrade the WebAuthn library on Joomla 5.
  78   *
  79   * @since   4.2.0
  80   *
  81   * @deprecated 5.0 We will upgrade the WebAuthn library to version 3 or later and this will go away.
  82   */
  83  class Server extends \Webauthn\Server
  84  {
  85      /**
  86       * @var   integer
  87       * @since 4.2.0
  88       */
  89      public $timeout = 60000;
  90  
  91      /**
  92       * @var   integer
  93       * @since 4.2.0
  94       */
  95      public $challengeSize = 32;
  96  
  97      /**
  98       * @var   PublicKeyCredentialRpEntity
  99       * @since 4.2.0
 100       */
 101      private $rpEntity;
 102  
 103      /**
 104       * @var   ManagerFactory
 105       * @since 4.2.0
 106       */
 107      private $coseAlgorithmManagerFactory;
 108  
 109      /**
 110       * @var   PublicKeyCredentialSourceRepository
 111       * @since 4.2.0
 112       */
 113      private $publicKeyCredentialSourceRepository;
 114  
 115      /**
 116       * @var   TokenBindingNotSupportedHandler
 117       * @since 4.2.0
 118       */
 119      private $tokenBindingHandler;
 120  
 121      /**
 122       * @var   ExtensionOutputCheckerHandler
 123       * @since 4.2.0
 124       */
 125      private $extensionOutputCheckerHandler;
 126  
 127      /**
 128       * @var   string[]
 129       * @since 4.2.0
 130       */
 131      private $selectedAlgorithms;
 132  
 133      /**
 134       * @var   MetadataStatementRepository|null
 135       * @since 4.2.0
 136       */
 137      private $metadataStatementRepository;
 138  
 139      /**
 140       * @var   ClientInterface
 141       * @since 4.2.0
 142       */
 143      private $httpClient;
 144  
 145      /**
 146       * @var   string
 147       * @since 4.2.0
 148       */
 149      private $googleApiKey;
 150  
 151      /**
 152       * @var   RequestFactoryInterface
 153       * @since 4.2.0
 154       */
 155      private $requestFactory;
 156  
 157      /**
 158       * Overridden constructor.
 159       *
 160       * @param   PublicKeyCredentialRpEntity          $relayingParty                       Obvious
 161       * @param   PublicKeyCredentialSourceRepository  $publicKeyCredentialSourceRepository Obvious
 162       * @param   MetadataStatementRepository|null     $metadataStatementRepository         Obvious
 163       *
 164       * @since   4.2.0
 165       */
 166      public function __construct(
 167          PublicKeyCredentialRpEntity $relayingParty,
 168          PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository,
 169          ?MetadataStatementRepository $metadataStatementRepository
 170      ) {
 171          $this->rpEntity = $relayingParty;
 172  
 173          $this->coseAlgorithmManagerFactory = new ManagerFactory();
 174          $this->coseAlgorithmManagerFactory->add('RS1', new RSA\RS1());
 175          $this->coseAlgorithmManagerFactory->add('RS256', new RSA\RS256());
 176          $this->coseAlgorithmManagerFactory->add('RS384', new RSA\RS384());
 177          $this->coseAlgorithmManagerFactory->add('RS512', new RSA\RS512());
 178          $this->coseAlgorithmManagerFactory->add('PS256', new RSA\PS256());
 179          $this->coseAlgorithmManagerFactory->add('PS384', new RSA\PS384());
 180          $this->coseAlgorithmManagerFactory->add('PS512', new RSA\PS512());
 181          $this->coseAlgorithmManagerFactory->add('ES256', new ECDSA\ES256());
 182          $this->coseAlgorithmManagerFactory->add('ES256K', new ECDSA\ES256K());
 183          $this->coseAlgorithmManagerFactory->add('ES384', new ECDSA\ES384());
 184          $this->coseAlgorithmManagerFactory->add('ES512', new ECDSA\ES512());
 185          $this->coseAlgorithmManagerFactory->add('Ed25519', new EdDSA\Ed25519());
 186  
 187          $this->selectedAlgorithms = ['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512', 'Ed25519'];
 188          $this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
 189          $this->tokenBindingHandler = new TokenBindingNotSupportedHandler();
 190          $this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
 191          $this->metadataStatementRepository = $metadataStatementRepository;
 192      }
 193  
 194      /**
 195       * @param   string[]  $selectedAlgorithms  Obvious
 196       *
 197       * @return  void
 198       * @since   4.2.0
 199       */
 200      public function setSelectedAlgorithms(array $selectedAlgorithms): void
 201      {
 202          $this->selectedAlgorithms = $selectedAlgorithms;
 203      }
 204  
 205      /**
 206       * @param   TokenBindingNotSupportedHandler  $tokenBindingHandler Obvious
 207       *
 208       * @return  void
 209       * @since   4.2.0
 210       */
 211      public function setTokenBindingHandler(TokenBindingNotSupportedHandler $tokenBindingHandler): void
 212      {
 213          $this->tokenBindingHandler = $tokenBindingHandler;
 214      }
 215  
 216      /**
 217       * @param   string     $alias      Obvious
 218       * @param   Algorithm  $algorithm  Obvious
 219       *
 220       * @return  void
 221       * @since   4.2.0
 222       */
 223      public function addAlgorithm(string $alias, Algorithm $algorithm): void
 224      {
 225          $this->coseAlgorithmManagerFactory->add($alias, $algorithm);
 226          $this->selectedAlgorithms[] = $alias;
 227          $this->selectedAlgorithms = array_unique($this->selectedAlgorithms);
 228      }
 229  
 230      /**
 231       * @param   ExtensionOutputCheckerHandler  $extensionOutputCheckerHandler Obvious
 232       *
 233       * @return  void
 234       * @since   4.2.0
 235       */
 236      public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void
 237      {
 238          $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
 239      }
 240  
 241      /**
 242       * @param   string|null                                $userVerification             Obvious
 243       * @param   PublicKeyCredentialDescriptor[]            $allowedPublicKeyDescriptors  Obvious
 244       * @param   AuthenticationExtensionsClientInputs|null  $extensions                   Obvious
 245       *
 246       * @return PublicKeyCredentialRequestOptions
 247       * @throws \Exception
 248       * @since   4.2.0
 249       */
 250      public function generatePublicKeyCredentialRequestOptions(
 251          ?string $userVerification = PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED,
 252          array $allowedPublicKeyDescriptors = [],
 253          ?AuthenticationExtensionsClientInputs $extensions = null
 254      ): PublicKeyCredentialRequestOptions {
 255          return new PublicKeyCredentialRequestOptions(
 256              random_bytes($this->challengeSize),
 257              $this->timeout,
 258              $this->rpEntity->getId(),
 259              $allowedPublicKeyDescriptors,
 260              $userVerification,
 261              $extensions ?? new AuthenticationExtensionsClientInputs()
 262          );
 263      }
 264  
 265      /**
 266       * @param   PublicKeyCredentialUserEntity              $userEntity                    Obvious
 267       * @param   string|null                                $attestationMode               Obvious
 268       * @param   PublicKeyCredentialDescriptor[]            $excludedPublicKeyDescriptors  Obvious
 269       * @param   AuthenticatorSelectionCriteria|null        $criteria                      Obvious
 270       * @param   AuthenticationExtensionsClientInputs|null  $extensions                    Obvious
 271       *
 272       * @return  PublicKeyCredentialCreationOptions
 273       * @throws  \Exception
 274       * @since   4.2.0
 275       */
 276      public function generatePublicKeyCredentialCreationOptions(
 277          PublicKeyCredentialUserEntity $userEntity,
 278          ?string $attestationMode = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
 279          array $excludedPublicKeyDescriptors = [],
 280          ?AuthenticatorSelectionCriteria $criteria = null,
 281          ?AuthenticationExtensionsClientInputs $extensions = null
 282      ): PublicKeyCredentialCreationOptions {
 283          $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms);
 284          $publicKeyCredentialParametersList = [];
 285  
 286          foreach ($coseAlgorithmManager->all() as $algorithm) {
 287              $publicKeyCredentialParametersList[] = new PublicKeyCredentialParameters(
 288                  PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
 289                  $algorithm::identifier()
 290              );
 291          }
 292  
 293          $criteria   = $criteria ?? new AuthenticatorSelectionCriteria();
 294          $extensions = $extensions ?? new AuthenticationExtensionsClientInputs();
 295          $challenge  = random_bytes($this->challengeSize);
 296  
 297          return new PublicKeyCredentialCreationOptions(
 298              $this->rpEntity,
 299              $userEntity,
 300              $challenge,
 301              $publicKeyCredentialParametersList,
 302              $this->timeout,
 303              $excludedPublicKeyDescriptors,
 304              $criteria,
 305              $attestationMode,
 306              $extensions
 307          );
 308      }
 309  
 310      /**
 311       * @param   string                              $data                                Obvious
 312       * @param   PublicKeyCredentialCreationOptions  $publicKeyCredentialCreationOptions  Obvious
 313       * @param   ServerRequestInterface              $serverRequest                       Obvious
 314       *
 315       * @return  PublicKeyCredentialSource
 316       * @throws  \Assert\AssertionFailedException
 317       * @since   4.2.0
 318       */
 319      public function loadAndCheckAttestationResponse(
 320          string $data,
 321          PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
 322          ServerRequestInterface $serverRequest
 323      ): PublicKeyCredentialSource {
 324          $attestationStatementSupportManager = $this->getAttestationStatementSupportManager();
 325          $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
 326          $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
 327  
 328          $publicKeyCredential   = $publicKeyCredentialLoader->load($data);
 329          $authenticatorResponse = $publicKeyCredential->getResponse();
 330          Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAttestationResponse::class, 'Not an authenticator attestation response');
 331  
 332          $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
 333              $attestationStatementSupportManager,
 334              $this->publicKeyCredentialSourceRepository,
 335              $this->tokenBindingHandler,
 336              $this->extensionOutputCheckerHandler
 337          );
 338  
 339          return $authenticatorAttestationResponseValidator->check($authenticatorResponse, $publicKeyCredentialCreationOptions, $serverRequest);
 340      }
 341  
 342      /**
 343       * @param   string                              $data                               Obvious
 344       * @param   PublicKeyCredentialRequestOptions   $publicKeyCredentialRequestOptions  Obvious
 345       * @param   PublicKeyCredentialUserEntity|null  $userEntity                         Obvious
 346       * @param   ServerRequestInterface              $serverRequest                      Obvious
 347       *
 348       * @return  PublicKeyCredentialSource
 349       * @throws  \Assert\AssertionFailedException
 350       * @since   4.2.0
 351       */
 352      public function loadAndCheckAssertionResponse(
 353          string $data,
 354          PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
 355          ?PublicKeyCredentialUserEntity $userEntity,
 356          ServerRequestInterface $serverRequest
 357      ): PublicKeyCredentialSource {
 358          $attestationStatementSupportManager = $this->getAttestationStatementSupportManager();
 359          $attestationObjectLoader            = new AttestationObjectLoader($attestationStatementSupportManager);
 360          $publicKeyCredentialLoader          = new PublicKeyCredentialLoader($attestationObjectLoader);
 361  
 362          $publicKeyCredential   = $publicKeyCredentialLoader->load($data);
 363          $authenticatorResponse = $publicKeyCredential->getResponse();
 364          Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAssertionResponse::class, 'Not an authenticator assertion response');
 365  
 366          $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
 367              $this->publicKeyCredentialSourceRepository,
 368              null,
 369              $this->tokenBindingHandler,
 370              $this->extensionOutputCheckerHandler,
 371              $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms)
 372          );
 373  
 374          return $authenticatorAssertionResponseValidator->check(
 375              $publicKeyCredential->getRawId(),
 376              $authenticatorResponse,
 377              $publicKeyCredentialRequestOptions,
 378              $serverRequest,
 379              null !== $userEntity ? $userEntity->getId() : null
 380          );
 381      }
 382  
 383      /**
 384       * @param   ClientInterface          $client          Obvious
 385       * @param   string                   $apiKey          Obvious
 386       * @param   RequestFactoryInterface  $requestFactory  Obvious
 387       *
 388       * @return  void
 389       * @since   4.2.0
 390       */
 391      public function enforceAndroidSafetyNetVerification(
 392          ClientInterface $client,
 393          string $apiKey,
 394          RequestFactoryInterface $requestFactory
 395      ): void {
 396          $this->httpClient     = $client;
 397          $this->googleApiKey   = $apiKey;
 398          $this->requestFactory = $requestFactory;
 399      }
 400  
 401      /**
 402       * @return  AttestationStatementSupportManager
 403       * @since   4.2.0
 404       */
 405      private function getAttestationStatementSupportManager(): AttestationStatementSupportManager
 406      {
 407          $attestationStatementSupportManager = new AttestationStatementSupportManager();
 408          $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
 409  
 410          if ($this->metadataStatementRepository !== null) {
 411              $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms);
 412              $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport(null, $this->metadataStatementRepository));
 413  
 414              /**
 415               * Work around a third party library (web-token/jwt-signature-algorithm-eddsa) bug.
 416               *
 417               * On PHP 8 libsodium is compiled into PHP, it is not an extension. However, the third party library does
 418               * not check if the libsodium function are available; it checks if the "sodium" extension is loaded. This of
 419               * course causes an immediate failure with a Runtime exception EVEN IF the attested data isn't attested by
 420               * Android Safety Net. Therefore we have to not even load the AndroidSafetyNetAttestationStatementSupport
 421               * class in this case...
 422               */
 423              if (function_exists('sodium_crypto_sign_seed_keypair') && function_exists('extension_loaded') && extension_loaded('sodium')) {
 424                  $attestationStatementSupportManager->add(
 425                      new AndroidSafetyNetAttestationStatementSupport(
 426                          $this->httpClient,
 427                          $this->googleApiKey,
 428                          $this->requestFactory,
 429                          2000,
 430                          60000,
 431                          $this->metadataStatementRepository
 432                      )
 433                  );
 434              }
 435  
 436              $attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport(null, $this->metadataStatementRepository));
 437              $attestationStatementSupportManager->add(new TPMAttestationStatementSupport($this->metadataStatementRepository));
 438              $attestationStatementSupportManager->add(
 439                  new PackedAttestationStatementSupport(
 440                      null,
 441                      $coseAlgorithmManager,
 442                      $this->metadataStatementRepository
 443                  )
 444              );
 445          }
 446  
 447          return $attestationStatementSupportManager;
 448      }
 449  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer