PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
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

293 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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