PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.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 / AuthenticatorAttestationResponseValidator.php

AuthenticatorAttestationResponseValidator.php in Defender Security – Malware Scanner, Login Security & Firewall 5.3.1, at vendor/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php

385 lines 17.4 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;
15
16 use Assert\Assertion;
17 use function count;
18 use function in_array;
19 use InvalidArgumentException;
20 use function is_string;
21 use LogicException;
22 use Psr\Http\Message\ServerRequestInterface;
23 use Psr\Log\LoggerInterface;
24 use Psr\Log\NullLogger;
25 use Ramsey\Uuid\Uuid;
26 use function Safe\parse_url;
27 use function Safe\sprintf;
28 use Throwable;
29 use Webauthn\AttestationStatement\AttestationObject;
30 use Webauthn\AttestationStatement\AttestationStatement;
31 use Webauthn\AttestationStatement\AttestationStatementSupportManager;
32 use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
33 use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
34 use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
35 use Webauthn\CertificateChainChecker\CertificateChainChecker;
36 use Webauthn\MetadataService\MetadataStatement;
37 use Webauthn\MetadataService\MetadataStatementRepository;
38 use Webauthn\MetadataService\StatusReport;
39 use Webauthn\TokenBinding\TokenBindingHandler;
40 use Webauthn\TrustPath\CertificateTrustPath;
41 use Webauthn\TrustPath\EmptyTrustPath;
42
43 class AuthenticatorAttestationResponseValidator
44 {
45 /**
46 * @var AttestationStatementSupportManager
47 */
48 private $attestationStatementSupportManager;
49
50 /**
51 * @var PublicKeyCredentialSourceRepository
52 */
53 private $publicKeyCredentialSource;
54
55 /**
56 * @var TokenBindingHandler
57 */
58 private $tokenBindingHandler;
59
60 /**
61 * @var ExtensionOutputCheckerHandler
62 */
63 private $extensionOutputCheckerHandler;
64
65 /**
66 * @var LoggerInterface
67 */
68 private $logger;
69
70 /**
71 * @var MetadataStatementRepository|null
72 */
73 private $metadataStatementRepository;
74
75 /**
76 * @var CertificateChainChecker|null
77 */
78 private $certificateChainChecker;
79
80 public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, PublicKeyCredentialSourceRepository $publicKeyCredentialSource, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
81 {
82 if (null !== $logger) {
83 @trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
84 }
85 if (null !== $metadataStatementRepository) {
86 @trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatementRepository".', E_USER_DEPRECATED);
87 }
88 $this->attestationStatementSupportManager = $attestationStatementSupportManager;
89 $this->publicKeyCredentialSource = $publicKeyCredentialSource;
90 $this->tokenBindingHandler = $tokenBindingHandler;
91 $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
92 $this->metadataStatementRepository = $metadataStatementRepository;
93 $this->logger = $logger ?? new NullLogger();
94 }
95
96 public function setLogger(LoggerInterface $logger): self
97 {
98 $this->logger = $logger;
99
100 return $this;
101 }
102
103 public function setCertificateChainChecker(CertificateChainChecker $certificateChainChecker): self
104 {
105 $this->certificateChainChecker = $certificateChainChecker;
106
107 return $this;
108 }
109
110 public function setMetadataStatementRepository(MetadataStatementRepository $metadataStatementRepository): self
111 {
112 $this->metadataStatementRepository = $metadataStatementRepository;
113
114 return $this;
115 }
116
117 /**
118 * @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
119 */
120 public function check(AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $request, array $securedRelyingPartyId = []): PublicKeyCredentialSource
121 {
122 try {
123 $this->logger->info('Checking the authenticator attestation response', [
124 'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
125 'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
126 'host' => $request->getUri()->getHost(),
127 ]);
128 /** @see 7.1.1 */
129 //Nothing to do
130
131 /** @see 7.1.2 */
132 $C = $authenticatorAttestationResponse->getClientDataJSON();
133
134 /** @see 7.1.3 */
135 Assertion::eq('webauthn.create', $C->getType(), 'The client data type is not "webauthn.create".');
136
137 /** @see 7.1.4 */
138 Assertion::true(hash_equals($publicKeyCredentialCreationOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
139
140 /** @see 7.1.5 */
141 $rpId = $publicKeyCredentialCreationOptions->getRp()->getId() ?? $request->getUri()->getHost();
142 $facetId = $this->getFacetId($rpId, $publicKeyCredentialCreationOptions->getExtensions(), $authenticatorAttestationResponse->getAttestationObject()->getAuthData()->getExtensions());
143
144 $parsedRelyingPartyId = parse_url($C->getOrigin());
145 Assertion::isArray($parsedRelyingPartyId, sprintf('The origin URI "%s" is not valid', $C->getOrigin()));
146 Assertion::keyExists($parsedRelyingPartyId, 'scheme', 'Invalid origin rpId.');
147 $clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
148 Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
149 $rpIdLength = mb_strlen($facetId);
150 Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
151
152 if (!in_array($facetId, $securedRelyingPartyId, true)) {
153 $scheme = $parsedRelyingPartyId['scheme'] ?? '';
154 Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
155 }
156
157 /** @see 7.1.6 */
158 if (null !== $C->getTokenBinding()) {
159 $this->tokenBindingHandler->check($C->getTokenBinding(), $request);
160 }
161
162 /** @see 7.1.7 */
163 $clientDataJSONHash = hash('sha256', $authenticatorAttestationResponse->getClientDataJSON()->getRawData(), true);
164
165 /** @see 7.1.8 */
166 $attestationObject = $authenticatorAttestationResponse->getAttestationObject();
167
168 /** @see 7.1.9 */
169 $rpIdHash = hash('sha256', $facetId, true);
170 Assertion::true(hash_equals($rpIdHash, $attestationObject->getAuthData()->getRpIdHash()), 'rpId hash mismatch.');
171
172 /** @see 7.1.10 */
173 Assertion::true($attestationObject->getAuthData()->isUserPresent(), 'User was not present');
174 /** @see 7.1.11 */
175 if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialCreationOptions->getAuthenticatorSelection()->getUserVerification()) {
176 Assertion::true($attestationObject->getAuthData()->isUserVerified(), 'User authentication required.');
177 }
178
179 /** @see 7.1.12 */
180 $extensionsClientOutputs = $attestationObject->getAuthData()->getExtensions();
181 if (null !== $extensionsClientOutputs) {
182 $this->extensionOutputCheckerHandler->check(
183 $publicKeyCredentialCreationOptions->getExtensions(),
184 $extensionsClientOutputs
185 );
186 }
187
188 /** @see 7.1.13 */
189 $this->checkMetadataStatement($publicKeyCredentialCreationOptions, $attestationObject);
190 $fmt = $attestationObject->getAttStmt()->getFmt();
191 Assertion::true($this->attestationStatementSupportManager->has($fmt), 'Unsupported attestation statement format.');
192
193 /** @see 7.1.14 */
194 $attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
195 Assertion::true($attestationStatementSupport->isValid($clientDataJSONHash, $attestationObject->getAttStmt(), $attestationObject->getAuthData()), 'Invalid attestation statement.');
196
197 /** @see 7.1.15 */
198 /** @see 7.1.16 */
199 /** @see 7.1.17 */
200 Assertion::true($attestationObject->getAuthData()->hasAttestedCredentialData(), 'There is no attested credential data.');
201 $attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
202 Assertion::notNull($attestedCredentialData, 'There is no attested credential data.');
203 $credentialId = $attestedCredentialData->getCredentialId();
204 Assertion::null($this->publicKeyCredentialSource->findOneByCredentialId($credentialId), 'The credential ID already exists.');
205
206 /** @see 7.1.18 */
207 /** @see 7.1.19 */
208 $publicKeyCredentialSource = $this->createPublicKeyCredentialSource(
209 $credentialId,
210 $attestedCredentialData,
211 $attestationObject,
212 $publicKeyCredentialCreationOptions->getUser()->getId()
213 );
214 $this->logger->info('The attestation is valid');
215 $this->logger->debug('Public Key Credential Source', ['publicKeyCredentialSource' => $publicKeyCredentialSource]);
216
217 return $publicKeyCredentialSource;
218 } catch (Throwable $throwable) {
219 $this->logger->error('An error occurred', [
220 'exception' => $throwable,
221 ]);
222 throw $throwable;
223 }
224 }
225
226 private function checkCertificateChain(AttestationStatement $attestationStatement, ?MetadataStatement $metadataStatement): void
227 {
228 $trustPath = $attestationStatement->getTrustPath();
229 if (!$trustPath instanceof CertificateTrustPath) {
230 return;
231 }
232 $authenticatorCertificates = $trustPath->getCertificates();
233
234 if (null === $metadataStatement) {
235 // @phpstan-ignore-next-line
236 null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, [], null);
237
238 return;
239 }
240
241 $metadataStatementCertificates = $metadataStatement->getAttestationRootCertificates();
242 $rootStatementCertificates = $metadataStatement->getRootCertificates();
243 foreach ($metadataStatementCertificates as $key => $metadataStatementCertificate) {
244 $metadataStatementCertificates[$key] = CertificateToolbox::fixPEMStructure($metadataStatementCertificate);
245 }
246 $trustedCertificates = array_merge(
247 $metadataStatementCertificates,
248 $rootStatementCertificates
249 );
250
251 // @phpstan-ignore-next-line
252 null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates, $trustedCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, $trustedCertificates);
253 }
254
255 private function checkMetadataStatement(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, AttestationObject $attestationObject): void
256 {
257 $attestationStatement = $attestationObject->getAttStmt();
258 $attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
259 Assertion::notNull($attestedCredentialData, 'No attested credential data found');
260 $aaguid = $attestedCredentialData->getAaguid()->toString();
261 if (PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE === $publicKeyCredentialCreationOptions->getAttestation()) {
262 $this->logger->debug('No attestation is asked.');
263 //No attestation is asked. We shall ensure that the data is anonymous.
264 if (
265 '00000000-0000-0000-0000-000000000000' === $aaguid
266 && (AttestationStatement::TYPE_NONE === $attestationStatement->getType() || AttestationStatement::TYPE_SELF === $attestationStatement->getType())) {
267 $this->logger->debug('The Attestation Statement is anonymous.');
268 $this->checkCertificateChain($attestationStatement, null);
269
270 return;
271 }
272 $this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
273 'aaguid' => $aaguid,
274 'AttestationStatement' => $attestationStatement,
275 ]);
276 $attestedCredentialData->setAaguid(
277 Uuid::fromString('00000000-0000-0000-0000-000000000000')
278 );
279 $attestationObject->setAttStmt(AttestationStatement::createNone('none', [], new EmptyTrustPath()));
280
281 return;
282 }
283 if (AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
284 $this->logger->debug('No attestation returned.');
285 //No attestation is returned. We shall ensure that the AAGUID is a null one.
286 if ('00000000-0000-0000-0000-000000000000' !== $aaguid) {
287 $this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
288 'aaguid' => $aaguid,
289 'AttestationStatement' => $attestationStatement,
290 ]);
291 $attestedCredentialData->setAaguid(
292 Uuid::fromString('00000000-0000-0000-0000-000000000000')
293 );
294
295 return;
296 }
297
298 return;
299 }
300
301 //The MDS Repository is mandatory here
302 Assertion::notNull($this->metadataStatementRepository, 'The Metadata Statement Repository is mandatory when requesting attestation objects.');
303 $metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
304
305 // We check the last status report
306 $this->checkStatusReport(null === $metadataStatement ? [] : $metadataStatement->getStatusReports());
307
308 // We check the certificate chain (if any)
309 $this->checkCertificateChain($attestationStatement, $metadataStatement);
310
311 // If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
312 // => nothing to check
313 if ('00000000-0000-0000-0000-000000000000' === $aaguid || AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
314 return;
315 }
316
317 // At this point, the Metadata Statement is mandatory
318 Assertion::notNull($metadataStatement, sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid));
319
320 // Check Attestation Type is allowed
321 if (0 !== count($metadataStatement->getAttestationTypes())) {
322 $type = $this->getAttestationType($attestationStatement);
323 Assertion::inArray($type, $metadataStatement->getAttestationTypes(), 'Invalid attestation statement. The attestation type is not allowed for this authenticator');
324 }
325 }
326
327 /**
328 * @param StatusReport[] $statusReports
329 */
330 private function checkStatusReport(array $statusReports): void
331 {
332 if (0 !== count($statusReports)) {
333 $lastStatusReport = end($statusReports);
334 if ($lastStatusReport->isCompromised()) {
335 throw new LogicException('The authenticator is compromised and cannot be used');
336 }
337 }
338 }
339
340 private function createPublicKeyCredentialSource(string $credentialId, AttestedCredentialData $attestedCredentialData, AttestationObject $attestationObject, string $userHandle): PublicKeyCredentialSource
341 {
342 return new PublicKeyCredentialSource(
343 $credentialId,
344 PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
345 [],
346 $attestationObject->getAttStmt()->getType(),
347 $attestationObject->getAttStmt()->getTrustPath(),
348 $attestedCredentialData->getAaguid(),
349 $attestedCredentialData->getCredentialPublicKey(),
350 $userHandle,
351 $attestationObject->getAuthData()->getSignCount()
352 );
353 }
354
355 private function getAttestationType(AttestationStatement $attestationStatement): int
356 {
357 switch ($attestationStatement->getType()) {
358 case AttestationStatement::TYPE_BASIC:
359 return MetadataStatement::ATTESTATION_BASIC_FULL;
360 case AttestationStatement::TYPE_SELF:
361 return MetadataStatement::ATTESTATION_BASIC_SURROGATE;
362 case AttestationStatement::TYPE_ATTCA:
363 return MetadataStatement::ATTESTATION_ATTCA;
364 case AttestationStatement::TYPE_ECDAA:
365 return MetadataStatement::ATTESTATION_ECDAA;
366 default:
367 throw new InvalidArgumentException('Invalid attestation type');
368 }
369 }
370
371 private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
372 {
373 if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
374 return $rpId;
375 }
376 $appId = $authenticationExtensionsClientInputs->get('appid')->value();
377 $wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
378 if (!is_string($appId) || true !== $wasUsed) {
379 return $rpId;
380 }
381
382 return $appId;
383 }
384 }
385