[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

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

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


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