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