[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/multifactorauth/webauthn/src/Hotfix/ -> AndroidKeyAttestationStatementSupport.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 CBOR\Decoder;
  19  use CBOR\OtherObject\OtherObjectManager;
  20  use CBOR\Tag\TagObjectManager;
  21  use Cose\Algorithms;
  22  use Cose\Key\Ec2Key;
  23  use Cose\Key\Key;
  24  use Cose\Key\RsaKey;
  25  use FG\ASN1\ASNObject;
  26  use FG\ASN1\ExplicitlyTaggedObject;
  27  use FG\ASN1\Universal\OctetString;
  28  use FG\ASN1\Universal\Sequence;
  29  use Webauthn\AttestationStatement\AttestationStatement;
  30  use Webauthn\AttestationStatement\AttestationStatementSupport;
  31  use Webauthn\AuthenticatorData;
  32  use Webauthn\CertificateToolbox;
  33  use Webauthn\MetadataService\MetadataStatementRepository;
  34  use Webauthn\StringStream;
  35  use Webauthn\TrustPath\CertificateTrustPath;
  36  
  37  // phpcs:disable PSR1.Files.SideEffects
  38  \defined('_JEXEC') or die;
  39  // phpcs:enable PSR1.Files.SideEffects
  40  
  41  /**
  42   * We had to fork the key attestation support object from the WebAuthn server package to address an
  43   * issue with PHP 8.
  44   *
  45   * We are currently using an older version of the WebAuthn library (2.x) which was written before
  46   * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of
  47   * Joomla's Semantic Versioning promise.
  48   *
  49   * The AndroidKeyAttestationStatementSupport class forces an assertion on the result of the
  50   * openssl_pkey_get_public() function, assuming it will return a resource. However, starting with
  51   * PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a
  52   * result, you cannot use Android or FIDO U2F keys with WebAuthn.
  53   *
  54   * The assertion check is in a private method, therefore we have to fork both attestation support
  55   * class to change the assertion. The assertion takes place through a third party library we cannot
  56   * (and should not!) modify.
  57   *
  58   * @since   4.2.0
  59   *
  60   * @deprecated 5.0 We will upgrade the WebAuthn library to version 3 or later and this will go away.
  61   */
  62  final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport
  63  {
  64      /**
  65       * @var   Decoder
  66       * @since 4.2.0
  67       */
  68      private $decoder;
  69  
  70      /**
  71       * @var   MetadataStatementRepository|null
  72       * @since 4.2.0
  73       */
  74      private $metadataStatementRepository;
  75  
  76      /**
  77       * @param   Decoder|null                      $decoder                      Obvious
  78       * @param   MetadataStatementRepository|null  $metadataStatementRepository  Obvious
  79       *
  80       * @since   4.2.0
  81       */
  82      public function __construct(
  83          ?Decoder $decoder = null,
  84          ?MetadataStatementRepository $metadataStatementRepository = null
  85      ) {
  86          if ($decoder !== null) {
  87              @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED);
  88          }
  89  
  90          if ($metadataStatementRepository === null) {
  91              @trigger_error(
  92                  'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.',
  93                  E_USER_DEPRECATED
  94              );
  95          }
  96  
  97          $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager());
  98          $this->metadataStatementRepository = $metadataStatementRepository;
  99      }
 100  
 101      /**
 102       * @return  string
 103       * @since   4.2.0
 104       */
 105      public function name(): string
 106      {
 107          return 'android-key';
 108      }
 109  
 110      /**
 111       * @param   array  $attestation Obvious
 112       *
 113       * @return  AttestationStatement
 114       * @throws  \Assert\AssertionFailedException
 115       * @since   4.2.0
 116       */
 117      public function load(array $attestation): AttestationStatement
 118      {
 119          Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
 120  
 121          foreach (['sig', 'x5c', 'alg'] as $key) {
 122              Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
 123          }
 124  
 125          $certificates = $attestation['attStmt']['x5c'];
 126  
 127          Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
 128          Assertion::greaterThan(\count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
 129          Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
 130  
 131          $certificates = CertificateToolbox::convertAllDERToPEM($certificates);
 132  
 133          return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
 134      }
 135  
 136      /**
 137       * @param   string                $clientDataJSONHash    Obvious
 138       * @param   AttestationStatement  $attestationStatement  Obvious
 139       * @param   AuthenticatorData     $authenticatorData     Obvious
 140       *
 141       * @return  boolean
 142       * @throws  \Assert\AssertionFailedException
 143       * @since   4.2.0
 144       */
 145      public function isValid(
 146          string $clientDataJSONHash,
 147          AttestationStatement $attestationStatement,
 148          AuthenticatorData $authenticatorData
 149      ): bool {
 150          $trustPath = $attestationStatement->getTrustPath();
 151          Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
 152  
 153          $certificates = $trustPath->getCertificates();
 154  
 155          if ($this->metadataStatementRepository !== null) {
 156              $certificates = CertificateToolbox::checkAttestationMedata(
 157                  $attestationStatement,
 158                  $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
 159                  $certificates,
 160                  $this->metadataStatementRepository
 161              );
 162          }
 163  
 164          // Decode leaf attestation certificate
 165          $leaf = $certificates[0];
 166          $this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
 167  
 168          $signedData = $authenticatorData->getAuthData() . $clientDataJSONHash;
 169          $alg = $attestationStatement->get('alg');
 170  
 171          return openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg)) === 1;
 172      }
 173  
 174      /**
 175       * @param   string             $certificate        Obvious
 176       * @param   string             $clientDataHash     Obvious
 177       * @param   AuthenticatorData  $authenticatorData  Obvious
 178       *
 179       * @return  void
 180       * @throws  \Assert\AssertionFailedException
 181       * @throws  \FG\ASN1\Exception\ParserException
 182       * @since   4.2.0
 183       */
 184      private function checkCertificateAndGetPublicKey(
 185          string $certificate,
 186          string $clientDataHash,
 187          AuthenticatorData $authenticatorData
 188      ): void {
 189          $resource = openssl_pkey_get_public($certificate);
 190  
 191          if (version_compare(PHP_VERSION, '8.0', 'lt')) {
 192              Assertion::isResource($resource, 'Unable to read the certificate');
 193          } else {
 194              /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
 195              Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate');
 196          }
 197  
 198          $details = openssl_pkey_get_details($resource);
 199          Assertion::isArray($details, 'Unable to read the certificate');
 200  
 201          // Check that authData publicKey matches the public key in the attestation certificate
 202          $attestedCredentialData = $authenticatorData->getAttestedCredentialData();
 203          Assertion::notNull($attestedCredentialData, 'No attested credential data found');
 204          $publicKeyData = $attestedCredentialData->getCredentialPublicKey();
 205          Assertion::notNull($publicKeyData, 'No attested public key found');
 206          $publicDataStream = new StringStream($publicKeyData);
 207          $coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
 208          Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
 209          $publicDataStream->close();
 210          $publicKey = Key::createFromData($coseKey);
 211  
 212          Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
 213          Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key');
 214  
 215          $certDetails = openssl_x509_parse($certificate);
 216  
 217          // Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions
 218          Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
 219          Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
 220          Assertion::keyExists(
 221              $certDetails['extensions'],
 222              '1.3.6.1.4.1.11129.2.1.17',
 223              'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing'
 224          );
 225          $extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17'];
 226          $extensionAsAsn1 = ASNObject::fromBinary($extension);
 227          Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 228          $objects = $extensionAsAsn1->getChildren();
 229  
 230          // Check that attestationChallenge is set to the clientDataHash.
 231          Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 232          Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 233          Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid');
 234  
 235          // Check that both teeEnforced and softwareEnforced structures don’t contain allApplications(600) tag.
 236          Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 237          $softwareEnforcedFlags = $objects[6];
 238          Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 239          $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
 240  
 241          Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 242          $teeEnforcedFlags = $objects[6];
 243          Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
 244          $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags);
 245      }
 246  
 247      /**
 248       * @param   Sequence  $sequence Obvious
 249       *
 250       * @return  void
 251       * @throws  \Assert\AssertionFailedException
 252       * @since   4.2.0
 253       */
 254      private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void
 255      {
 256          foreach ($sequence->getChildren() as $tag) {
 257              Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag');
 258  
 259              /**
 260               * @var ExplicitlyTaggedObject $tag It is silly that I have to do that for PHPCS to be happy.
 261               */
 262              Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found');
 263          }
 264      }
 265  }


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