[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

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


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