[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/ -> TPMAttestationStatementSupport.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 Base64Url\Base64Url;
  18  use CBOR\Decoder;
  19  use CBOR\MapObject;
  20  use CBOR\OtherObject\OtherObjectManager;
  21  use CBOR\Tag\TagObjectManager;
  22  use Cose\Algorithms;
  23  use Cose\Key\Ec2Key;
  24  use Cose\Key\Key;
  25  use Cose\Key\OkpKey;
  26  use Cose\Key\RsaKey;
  27  use DateTimeImmutable;
  28  use InvalidArgumentException;
  29  use RuntimeException;
  30  use Webauthn\AuthenticatorData;
  31  use Webauthn\CertificateToolbox;
  32  use Webauthn\MetadataService\MetadataStatementRepository;
  33  use Webauthn\StringStream;
  34  use Webauthn\TrustPath\CertificateTrustPath;
  35  use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
  36  
  37  final class TPMAttestationStatementSupport implements AttestationStatementSupport
  38  {
  39      /**
  40       * @var MetadataStatementRepository|null
  41       */
  42      private $metadataStatementRepository;
  43  
  44      public function name(): string
  45      {
  46          return 'tpm';
  47      }
  48  
  49      public function __construct(?MetadataStatementRepository $metadataStatementRepository = null)
  50      {
  51          $this->metadataStatementRepository = $metadataStatementRepository;
  52      }
  53  
  54      public function load(array $attestation): AttestationStatement
  55      {
  56          Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
  57          Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported');
  58          foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
  59              Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
  60          }
  61          Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object');
  62  
  63          $certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
  64          Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object');
  65  
  66          $pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
  67          $pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit');
  68          $pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true);
  69          $attestedName = $pubAreaAttestedNameAlg.$pubAreaHash;
  70          Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name');
  71  
  72          $attestation['attStmt']['parsedCertInfo'] = $certInfo;
  73          $attestation['attStmt']['parsedPubArea'] = $pubArea;
  74  
  75          $certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
  76          Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
  77  
  78          return AttestationStatement::createAttCA(
  79              $this->name(),
  80              $attestation['attStmt'],
  81              new CertificateTrustPath($certificates)
  82          );
  83      }
  84  
  85      public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
  86      {
  87          $attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash;
  88          $attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true);
  89          Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash');
  90          $this->checkUniquePublicKey(
  91              $attestationStatement->get('parsedPubArea')['unique'],
  92              $authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()
  93          );
  94  
  95          switch (true) {
  96              case $attestationStatement->getTrustPath() instanceof CertificateTrustPath:
  97                  return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData);
  98              case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath:
  99                  return $this->processWithECDAA();
 100              default:
 101                  throw new InvalidArgumentException('Unsupported attestation statement');
 102          }
 103      }
 104  
 105      private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
 106      {
 107          $cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
 108          $publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
 109          Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key');
 110          $key = new Key($publicKey->getNormalizedData(false));
 111  
 112          switch ($key->type()) {
 113              case Key::TYPE_OKP:
 114                  $uniqueFromKey = (new OkpKey($key->getData()))->x();
 115                  break;
 116              case Key::TYPE_EC2:
 117                  $ec2Key = new Ec2Key($key->getData());
 118                  $uniqueFromKey = "\x04".$ec2Key->x().$ec2Key->y();
 119                  break;
 120              case Key::TYPE_RSA:
 121                  $uniqueFromKey = (new RsaKey($key->getData()))->n();
 122                  break;
 123              default:
 124                  throw new InvalidArgumentException('Invalid or unsupported key type.');
 125          }
 126  
 127          Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value');
 128      }
 129  
 130      private function checkCertInfo(string $data): array
 131      {
 132          $certInfo = new StringStream($data);
 133  
 134          $magic = $certInfo->read(4);
 135          Assertion::eq('ff544347', bin2hex($magic), 'Invalid attestation object');
 136  
 137          $type = $certInfo->read(2);
 138  
 139          $qualifiedSignerLength = unpack('n', $certInfo->read(2))[1];
 140          $qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored
 141  
 142          $extraDataLength = unpack('n', $certInfo->read(2))[1];
 143          $extraData = $certInfo->read($extraDataLength);
 144  
 145          $clockInfo = $certInfo->read(17); //Ignore
 146  
 147          $firmwareVersion = $certInfo->read(8);
 148  
 149          $attestedNameLength = unpack('n', $certInfo->read(2))[1];
 150          $attestedName = $certInfo->read($attestedNameLength);
 151  
 152          $attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1];
 153          $attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore
 154          Assertion::true($certInfo->isEOF(), 'Invalid certificate information. Presence of extra bytes.');
 155          $certInfo->close();
 156  
 157          return [
 158              'magic' => $magic,
 159              'type' => $type,
 160              'qualifiedSigner' => $qualifiedSigner,
 161              'extraData' => $extraData,
 162              'clockInfo' => $clockInfo,
 163              'firmwareVersion' => $firmwareVersion,
 164              'attestedName' => $attestedName,
 165              'attestedQualifiedName' => $attestedQualifiedName,
 166          ];
 167      }
 168  
 169      private function checkPubArea(string $data): array
 170      {
 171          $pubArea = new StringStream($data);
 172  
 173          $type = $pubArea->read(2);
 174  
 175          $nameAlg = $pubArea->read(2);
 176  
 177          $objectAttributes = $pubArea->read(4);
 178  
 179          $authPolicyLength = unpack('n', $pubArea->read(2))[1];
 180          $authPolicy = $pubArea->read($authPolicyLength);
 181  
 182          $parameters = $this->getParameters($type, $pubArea);
 183  
 184          $uniqueLength = unpack('n', $pubArea->read(2))[1];
 185          $unique = $pubArea->read($uniqueLength);
 186          Assertion::true($pubArea->isEOF(), 'Invalid public area. Presence of extra bytes.');
 187          $pubArea->close();
 188  
 189          return [
 190              'type' => $type,
 191              'nameAlg' => $nameAlg,
 192              'objectAttributes' => $objectAttributes,
 193              'authPolicy' => $authPolicy,
 194              'parameters' => $parameters,
 195              'unique' => $unique,
 196          ];
 197      }
 198  
 199      private function getParameters(string $type, StringStream $stream): array
 200      {
 201          switch (bin2hex($type)) {
 202              case '0001':
 203              case '0014':
 204              case '0016':
 205                  return [
 206                      'symmetric' => $stream->read(2),
 207                      'scheme' => $stream->read(2),
 208                      'keyBits' => unpack('n', $stream->read(2))[1],
 209                      'exponent' => $this->getExponent($stream->read(4)),
 210                  ];
 211              case '0018':
 212                  return [
 213                      'symmetric' => $stream->read(2),
 214                      'scheme' => $stream->read(2),
 215                      'curveId' => $stream->read(2),
 216                      'kdf' => $stream->read(2),
 217                  ];
 218              default:
 219                  throw new InvalidArgumentException('Unsupported type');
 220          }
 221      }
 222  
 223      private function getExponent(string $exponent): string
 224      {
 225          return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent;
 226      }
 227  
 228      private function convertCertificatesToPem(array $certificates): array
 229      {
 230          foreach ($certificates as $k => $v) {
 231              $tmp = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
 232              $tmp .= chunk_split(base64_encode($v), 64, PHP_EOL);
 233              $tmp .= '-----END CERTIFICATE-----'.PHP_EOL;
 234              $certificates[$k] = $tmp;
 235          }
 236  
 237          return $certificates;
 238      }
 239  
 240      private function getTPMHash(string $nameAlg): string
 241      {
 242          switch (bin2hex($nameAlg)) {
 243              case '0004':
 244                  return 'sha1'; //: "TPM_ALG_SHA1",
 245              case '000b':
 246                  return 'sha256'; //: "TPM_ALG_SHA256",
 247              case '000c':
 248                  return 'sha384'; //: "TPM_ALG_SHA384",
 249              case '000d':
 250                  return 'sha512'; //: "TPM_ALG_SHA512",
 251              default:
 252                  throw new InvalidArgumentException('Unsupported hash algorithm');
 253          }
 254      }
 255  
 256      private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
 257      {
 258          $trustPath = $attestationStatement->getTrustPath();
 259          Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
 260  
 261          $certificates = $trustPath->getCertificates();
 262          if (null !== $this->metadataStatementRepository) {
 263              $certificates = CertificateToolbox::checkAttestationMedata(
 264                  $attestationStatement,
 265                  $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
 266                  $certificates,
 267                  $this->metadataStatementRepository
 268              );
 269          }
 270  
 271          // Check certificate CA chain and returns the Attestation Certificate
 272          $this->checkCertificate($certificates[0], $authenticatorData);
 273  
 274          // Get the COSE algorithm identifier and the corresponding OpenSSL one
 275          $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
 276          $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
 277  
 278          $result = openssl_verify($attestationStatement->get('certInfo'), $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier);
 279  
 280          return 1 === $result;
 281      }
 282  
 283      private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
 284      {
 285          $parsed = openssl_x509_parse($attestnCert);
 286          Assertion::isArray($parsed, 'Invalid certificate');
 287  
 288          //Check version
 289          Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
 290  
 291          //Check subject field is empty
 292          Assertion::false(!isset($parsed['subject']) || !\is_array($parsed['subject']) || 0 !== \count($parsed['subject']), 'Invalid certificate name. The Subject should be empty');
 293  
 294          // Check period of validity
 295          Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.');
 296          Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.');
 297          $startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
 298          Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.');
 299  
 300          Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.');
 301          Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.');
 302          $endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
 303          Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.');
 304  
 305          //Check extensions
 306          Assertion::false(!isset($parsed['extensions']) || !\is_array($parsed['extensions']), 'Certificate extensions are missing');
 307  
 308          //Check subjectAltName
 309          Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing');
 310  
 311          //Check extendedKeyUsage
 312          Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing');
 313          Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid');
 314  
 315          // id-fido-gen-ce-aaguid OID check
 316          Assertion::false(\in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($authenticatorData->getAttestedCredentialData()->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate');
 317  
 318          // TODO: For attestationRoot in metadata.attestationRootCertificates, generate verification chain verifX5C by appending attestationRoot to the x5c. Try verifying verifX5C. If successful go to next step. If fail try next attestationRoot. If no attestationRoots left to try, fail.
 319      }
 320  
 321      private function processWithECDAA(): bool
 322      {
 323          throw new RuntimeException('ECDAA not supported');
 324      }
 325  }


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