[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/ -> AndroidSafetyNetAttestationStatementSupport.php (source)

   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  /*
   6   * The MIT License (MIT)
   7   *
   8   * Copyright (c) 2014-2019 Spomky-Labs
   9   *
  10   * This software may be modified and distributed under the terms
  11   * of the MIT license.  See the LICENSE file for details.
  12   */
  13  
  14  namespace Webauthn\AttestationStatement;
  15  
  16  use Assert\Assertion;
  17  use InvalidArgumentException;
  18  use Jose\Component\Core\AlgorithmManager;
  19  use Jose\Component\Core\Util\JsonConverter;
  20  use Jose\Component\KeyManagement\JWKFactory;
  21  use Jose\Component\Signature\Algorithm;
  22  use Jose\Component\Signature\JWS;
  23  use Jose\Component\Signature\JWSVerifier;
  24  use Jose\Component\Signature\Serializer\CompactSerializer;
  25  use Psr\Http\Client\ClientInterface;
  26  use Psr\Http\Message\RequestFactoryInterface;
  27  use Psr\Http\Message\ResponseInterface;
  28  use RuntimeException;
  29  use Webauthn\AuthenticatorData;
  30  use Webauthn\CertificateToolbox;
  31  use Webauthn\MetadataService\MetadataStatementRepository;
  32  use Webauthn\TrustPath\CertificateTrustPath;
  33  
  34  final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport
  35  {
  36      /**
  37       * @var string|null
  38       */
  39      private $apiKey;
  40  
  41      /**
  42       * @var ClientInterface|null
  43       */
  44      private $client;
  45  
  46      /**
  47       * @var CompactSerializer
  48       */
  49      private $jwsSerializer;
  50  
  51      /**
  52       * @var JWSVerifier|null
  53       */
  54      private $jwsVerifier;
  55  
  56      /**
  57       * @var RequestFactoryInterface|null
  58       */
  59      private $requestFactory;
  60  
  61      /**
  62       * @var int
  63       */
  64      private $leeway;
  65  
  66      /**
  67       * @var int
  68       */
  69      private $maxAge;
  70  
  71      /**
  72       * @var MetadataStatementRepository|null
  73       */
  74      private $metadataStatementRepository;
  75  
  76      public function __construct(?ClientInterface $client = null, ?string $apiKey = null, ?RequestFactoryInterface $requestFactory = null, int $leeway = 0, int $maxAge = 60000, ?MetadataStatementRepository $metadataStatementRepository = null)
  77      {
  78          foreach ([Algorithm\RS256::class] as $algorithm) {
  79              if (!class_exists($algorithm)) {
  80                  throw new RuntimeException('The algorithms RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?');
  81              }
  82          }
  83          $this->jwsSerializer = new CompactSerializer();
  84          $this->apiKey = $apiKey;
  85          $this->client = $client;
  86          $this->requestFactory = $requestFactory;
  87          $this->initJwsVerifier();
  88          $this->leeway = $leeway;
  89          $this->maxAge = $maxAge;
  90          $this->metadataStatementRepository = $metadataStatementRepository;
  91      }
  92  
  93      public function name(): string
  94      {
  95          return 'android-safetynet';
  96      }
  97  
  98      public function load(array $attestation): AttestationStatement
  99      {
 100          Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
 101          foreach (['ver', 'response'] as $key) {
 102              Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
 103              Assertion::notEmpty($attestation['attStmt'][$key], sprintf('The attestation statement value "%s" is empty.', $key));
 104          }
 105          $jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
 106          $jwsHeader = $jws->getSignature(0)->getProtectedHeader();
 107          Assertion::keyExists($jwsHeader, 'x5c', 'The response in the attestation statement must contain a "x5c" header.');
 108          Assertion::notEmpty($jwsHeader['x5c'], 'The "x5c" parameter in the attestation statement response must contain at least one certificate.');
 109          $certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
 110          $attestation['attStmt']['jws'] = $jws;
 111  
 112          return AttestationStatement::createBasic(
 113              $this->name(),
 114              $attestation['attStmt'],
 115              new CertificateTrustPath($certificates)
 116          );
 117      }
 118  
 119      public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
 120      {
 121          $trustPath = $attestationStatement->getTrustPath();
 122          Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
 123          $certificates = $trustPath->getCertificates();
 124          if (null !== $this->metadataStatementRepository) {
 125              $certificates = CertificateToolbox::checkAttestationMedata(
 126                  $attestationStatement,
 127                  $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
 128                  $certificates,
 129                  $this->metadataStatementRepository
 130              );
 131          }
 132  
 133          $parsedCertificate = openssl_x509_parse(current($certificates));
 134          Assertion::isArray($parsedCertificate, 'Invalid attestation object');
 135          Assertion::keyExists($parsedCertificate, 'subject', 'Invalid attestation object');
 136          Assertion::keyExists($parsedCertificate['subject'], 'CN', 'Invalid attestation object');
 137          Assertion::eq($parsedCertificate['subject']['CN'], 'attest.android.com', 'Invalid attestation object');
 138  
 139          /** @var JWS $jws */
 140          $jws = $attestationStatement->get('jws');
 141          $payload = $jws->getPayload();
 142          $this->validatePayload($payload, $clientDataJSONHash, $authenticatorData);
 143  
 144          //Check the signature
 145          $this->validateSignature($jws, $trustPath);
 146  
 147          //Check against Google service
 148          $this->validateUsingGoogleApi($attestationStatement);
 149  
 150          return true;
 151      }
 152  
 153      private function validatePayload(?string $payload, string $clientDataJSONHash, AuthenticatorData $authenticatorData): void
 154      {
 155          Assertion::notNull($payload, 'Invalid attestation object');
 156          $payload = JsonConverter::decode($payload);
 157          Assertion::isArray($payload, 'Invalid attestation object');
 158          Assertion::keyExists($payload, 'nonce', 'Invalid attestation object. "nonce" is missing.');
 159          Assertion::eq($payload['nonce'], base64_encode(hash('sha256', $authenticatorData->getAuthData().$clientDataJSONHash, true)), 'Invalid attestation object. Invalid nonce');
 160          Assertion::keyExists($payload, 'ctsProfileMatch', 'Invalid attestation object. "ctsProfileMatch" is missing.');
 161          Assertion::true($payload['ctsProfileMatch'], 'Invalid attestation object. "ctsProfileMatch" value is false.');
 162          Assertion::keyExists($payload, 'timestampMs', 'Invalid attestation object. Timestamp is missing.');
 163          Assertion::integer($payload['timestampMs'], 'Invalid attestation object. Timestamp shall be an integer.');
 164          $currentTime = time() * 1000;
 165          Assertion::lessOrEqualThan($payload['timestampMs'], $currentTime + $this->leeway, sprintf('Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
 166          Assertion::lessOrEqualThan($currentTime - $payload['timestampMs'], $this->maxAge, sprintf('Invalid attestation object. Too old. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
 167      }
 168  
 169      private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void
 170      {
 171          $jwk = JWKFactory::createFromCertificate($trustPath->getCertificates()[0]);
 172          $isValid = $this->jwsVerifier->verifyWithKey($jws, $jwk, 0);
 173          Assertion::true($isValid, 'Invalid response signature');
 174      }
 175  
 176      private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
 177      {
 178          if (null === $this->client || null === $this->apiKey || null === $this->requestFactory) {
 179              return;
 180          }
 181          $uri = sprintf('https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s', urlencode($this->apiKey));
 182          $requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
 183          $request = $this->requestFactory->createRequest('POST', $uri);
 184          $request = $request->withHeader('content-type', 'application/json');
 185          $request->getBody()->write($requestBody);
 186  
 187          $response = $this->client->sendRequest($request);
 188          $this->checkGoogleApiResponse($response);
 189          $responseBody = $this->getResponseBody($response);
 190          $responseBodyJson = json_decode($responseBody, true);
 191          Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid response.');
 192          Assertion::keyExists($responseBodyJson, 'isValidSignature', 'Invalid response.');
 193          Assertion::boolean($responseBodyJson['isValidSignature'], 'Invalid response.');
 194          Assertion::true($responseBodyJson['isValidSignature'], 'Invalid response.');
 195      }
 196  
 197      private function getResponseBody(ResponseInterface $response): string
 198      {
 199          $responseBody = '';
 200          $response->getBody()->rewind();
 201          do {
 202              $tmp = $response->getBody()->read(1024);
 203              if ('' === $tmp) {
 204                  break;
 205              }
 206              $responseBody .= $tmp;
 207          } while (true);
 208  
 209          return $responseBody;
 210      }
 211  
 212      private function checkGoogleApiResponse(ResponseInterface $response): void
 213      {
 214          Assertion::eq(200, $response->getStatusCode(), 'Request did not succeeded');
 215          Assertion::true($response->hasHeader('content-type'), 'Unrecognized response');
 216  
 217          foreach ($response->getHeader('content-type') as $header) {
 218              if (0 === mb_strpos($header, 'application/json')) {
 219                  return;
 220              }
 221          }
 222  
 223          throw new InvalidArgumentException('Unrecognized response');
 224      }
 225  
 226      private function convertCertificatesToPem(array $certificates): array
 227      {
 228          foreach ($certificates as $k => $v) {
 229              $certificates[$k] = CertificateToolbox::fixPEMStructure($v);
 230          }
 231  
 232          return $certificates;
 233      }
 234  
 235      private function initJwsVerifier(): void
 236      {
 237          $algorithmClasses = [
 238              Algorithm\RS256::class, Algorithm\RS384::class, Algorithm\RS512::class,
 239              Algorithm\PS256::class, Algorithm\PS384::class, Algorithm\PS512::class,
 240              Algorithm\ES256::class, Algorithm\ES384::class, Algorithm\ES512::class,
 241              Algorithm\EdDSA::class,
 242          ];
 243          $algorithms = [];
 244          foreach ($algorithmClasses as $key => $algorithm) {
 245              if (class_exists($algorithm)) {
 246                  $algorithms[] = new $algorithm();
 247              }
 248          }
 249          $algorithmManager = new AlgorithmManager($algorithms);
 250          $this->jwsVerifier = new JWSVerifier($algorithmManager);
 251      }
 252  }


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