[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |