PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
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 / lib / packages / Webauthn / AuthenticatorAssertionResponseValidator.php

AuthenticatorAssertionResponseValidator.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.2, at lib/packages/Webauthn/AuthenticatorAssertionResponseValidator.php

273 lines 12.3 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 WP_DEFENDER_VENDOR\Webauthn;
15
16 use WP_DEFENDER_VENDOR\Assert\Assertion;
17 use WP_DEFENDER_VENDOR\CBOR\Decoder;
18 use WP_DEFENDER_VENDOR\CBOR\OtherObject\OtherObjectManager;
19 use WP_DEFENDER_VENDOR\CBOR\Tag\TagObjectManager;
20 use WP_DEFENDER_VENDOR\Cose\Algorithm\Manager;
21 use WP_DEFENDER_VENDOR\Cose\Algorithm\Signature\Signature;
22 use WP_DEFENDER_VENDOR\Cose\Key\Key;
23 use function count;
24 use function in_array;
25 use function is_string;
26 use WP_DEFENDER_VENDOR\Psr\Http\Message\ServerRequestInterface;
27 use WP_DEFENDER_VENDOR\Psr\Log\LoggerInterface;
28 use WP_DEFENDER_VENDOR\Psr\Log\NullLogger;
29 use function WP_DEFENDER_VENDOR\Safe\parse_url;
30 use Throwable;
31 use WP_DEFENDER_VENDOR\Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
32 use WP_DEFENDER_VENDOR\Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
33 use WP_DEFENDER_VENDOR\Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
34 use WP_DEFENDER_VENDOR\Webauthn\Counter\CounterChecker;
35 use WP_DEFENDER_VENDOR\Webauthn\Counter\ThrowExceptionIfInvalid;
36 use WP_DEFENDER_VENDOR\Webauthn\TokenBinding\TokenBindingHandler;
37 use WP_DEFENDER_VENDOR\Webauthn\Util\CoseSignatureFixer;
38
39 class AuthenticatorAssertionResponseValidator
40 {
41 /**
42 * @var PublicKeyCredentialSourceRepository
43 */
44 private $publicKeyCredentialSourceRepository;
45
46 /**
47 * @var Decoder
48 */
49 private $decoder;
50
51 /**
52 * @var TokenBindingHandler
53 */
54 private $tokenBindingHandler;
55
56 /**
57 * @var ExtensionOutputCheckerHandler
58 */
59 private $extensionOutputCheckerHandler;
60
61 /**
62 * @var Manager|null
63 */
64 private $algorithmManager;
65
66 /**
67 * @var CounterChecker
68 */
69 private $counterChecker;
70
71 /**
72 * @var LoggerInterface|null
73 */
74 private $logger;
75
76 public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, Manager $algorithmManager, ?CounterChecker $counterChecker = null, ?LoggerInterface $logger = null)
77 {
78 if (null !== $logger) {
79 @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);
80 }
81 if (null !== $counterChecker) {
82 @trigger_error('The argument "counterChecker" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setCounterChecker".', E_USER_DEPRECATED);
83 }
84 $this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
85 $this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
86 $this->tokenBindingHandler = $tokenBindingHandler;
87 $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
88 $this->algorithmManager = $algorithmManager;
89 $this->counterChecker = $counterChecker ?? new ThrowExceptionIfInvalid();
90 $this->logger = $logger ?? new NullLogger();
91 }
92
93 /**
94 * @see https://www.w3.org/TR/webauthn/#verifying-assertion
95 */
96 public function check(string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface $request, ?string $userHandle, array $securedRelyingPartyId = []): PublicKeyCredentialSource
97 {
98 try {
99 $this->logger->info('Checking the authenticator assertion response', [
100 'credentialId' => $credentialId,
101 'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
102 'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
103 'host' => $request->getUri()->getHost(),
104 'userHandle' => $userHandle,
105 ]);
106 /** @see 7.2.1 */
107 if (0 !== count($publicKeyCredentialRequestOptions->getAllowCredentials())) {
108 Assertion::true($this->isCredentialIdAllowed($credentialId, $publicKeyCredentialRequestOptions->getAllowCredentials()), 'The credential ID is not allowed.');
109 }
110
111 /** @see 7.2.2 */
112 $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($credentialId);
113 Assertion::notNull($publicKeyCredentialSource, 'The credential ID is invalid.');
114
115 /** @see 7.2.3 */
116 $attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
117 $credentialUserHandle = $publicKeyCredentialSource->getUserHandle();
118 $responseUserHandle = $authenticatorAssertionResponse->getUserHandle();
119
120 /** @see 7.2.2 User Handle*/
121 if (null !== $userHandle) { //If the user was identified before the authentication ceremony was initiated,
122 Assertion::eq($credentialUserHandle, $userHandle, 'Invalid user handle');
123 if (null !== $responseUserHandle && '' !== $responseUserHandle) {
124 Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
125 }
126 } else {
127 Assertion::notEmpty($responseUserHandle, 'User handle is mandatory');
128 Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
129 }
130
131 $credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
132 $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
133 if ($isU2F) {
134 $credentialPublicKey = U2FPublicKey::createCOSEKey($credentialPublicKey);
135 }
136 Assertion::notNull($credentialPublicKey, 'No public key available.');
137 $stream = new StringStream($credentialPublicKey);
138 $credentialPublicKeyStream = $this->decoder->decode($stream);
139 Assertion::true($stream->isEOF(), 'Invalid key. Presence of extra bytes.');
140 $stream->close();
141
142 /** @see 7.2.4 */
143 /** @see 7.2.5 */
144 //Nothing to do. Use of objects directly
145
146 /** @see 7.2.6 */
147 $C = $authenticatorAssertionResponse->getClientDataJSON();
148
149 /** @see 7.2.7 */
150 Assertion::eq('webauthn.get', $C->getType(), 'The client data type is not "webauthn.get".');
151
152 /** @see 7.2.8 */
153 Assertion::true(hash_equals($publicKeyCredentialRequestOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
154
155 /** @see 7.2.9 */
156 $rpId = $publicKeyCredentialRequestOptions->getRpId() ?? $request->getUri()->getHost();
157 $facetId = $this->getFacetId($rpId, $publicKeyCredentialRequestOptions->getExtensions(), $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions());
158 $parsedRelyingPartyId = parse_url($C->getOrigin());
159 Assertion::isArray($parsedRelyingPartyId, 'Invalid origin');
160 if (!in_array($facetId, $securedRelyingPartyId, true)) {
161 $scheme = $parsedRelyingPartyId['scheme'] ?? '';
162 Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
163 }
164 $clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
165 Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
166 $rpIdLength = mb_strlen($facetId);
167 Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
168
169 /** @see 7.2.10 */
170 if (null !== $C->getTokenBinding()) {
171 $this->tokenBindingHandler->check($C->getTokenBinding(), $request);
172 }
173
174 $expectedRpIdHash = $isU2F ? $C->getOrigin() : $facetId;
175 // u2f response has full origin in rpIdHash
176 /** @see 7.2.11 */
177 $rpIdHash = hash('sha256', $expectedRpIdHash, true);
178 Assertion::true(hash_equals($rpIdHash, $authenticatorAssertionResponse->getAuthenticatorData()->getRpIdHash()), 'rpId hash mismatch.');
179
180 /** @see 7.2.12 */
181 Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserPresent(), 'User was not present');
182 /** @see 7.2.13 */
183 if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialRequestOptions->getUserVerification()) {
184 Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserVerified(), 'User authentication required.');
185 }
186
187 /** @see 7.2.14 */
188 $extensionsClientOutputs = $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions();
189 if (null !== $extensionsClientOutputs) {
190 $this->extensionOutputCheckerHandler->check(
191 $publicKeyCredentialRequestOptions->getExtensions(),
192 $extensionsClientOutputs
193 );
194 }
195
196 /** @see 7.2.15 */
197 $getClientDataJSONHash = hash('sha256', $authenticatorAssertionResponse->getClientDataJSON()->getRawData(), true);
198
199 /** @see 7.2.16 */
200 $dataToVerify = $authenticatorAssertionResponse->getAuthenticatorData()->getAuthData().$getClientDataJSONHash;
201 $signature = $authenticatorAssertionResponse->getSignature();
202 $coseKey = new Key($credentialPublicKeyStream->getNormalizedData());
203 $algorithm = $this->algorithmManager->get($coseKey->alg());
204 Assertion::isInstanceOf($algorithm, Signature::class, 'Invalid algorithm identifier. Should refer to a signature algorithm');
205 $signature = CoseSignatureFixer::fix($signature, $algorithm);
206 Assertion::true($algorithm->verify($dataToVerify, $coseKey, $signature), 'Invalid signature.');
207
208 /** @see 7.2.17 */
209 $storedCounter = $publicKeyCredentialSource->getCounter();
210 $responseCounter = $authenticatorAssertionResponse->getAuthenticatorData()->getSignCount();
211 if (0 !== $responseCounter || 0 !== $storedCounter) {
212 $this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
213 }
214 $publicKeyCredentialSource->setCounter($responseCounter);
215 $this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
216
217 /** @see 7.2.18 */
218 //All good. We can continue.
219 $this->logger->info('The assertion is valid');
220 $this->logger->debug('Public Key Credential Source', ['publicKeyCredentialSource' => $publicKeyCredentialSource]);
221
222 return $publicKeyCredentialSource;
223 } catch (Throwable $throwable) {
224 $this->logger->error('An error occurred', [
225 'exception' => $throwable,
226 ]);
227 throw $throwable;
228 }
229 }
230
231 public function setLogger(LoggerInterface $logger): self
232 {
233 $this->logger = $logger;
234
235 return $this;
236 }
237
238 public function setCounterChecker(CounterChecker $counterChecker): self
239 {
240 $this->counterChecker = $counterChecker;
241
242 return $this;
243 }
244
245 /**
246 * @param array<PublicKeyCredentialDescriptor> $allowedCredentials
247 */
248 private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool
249 {
250 foreach ($allowedCredentials as $allowedCredential) {
251 if (hash_equals($allowedCredential->getId(), $credentialId)) {
252 return true;
253 }
254 }
255
256 return false;
257 }
258
259 private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
260 {
261 if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
262 return $rpId;
263 }
264 $appId = $authenticationExtensionsClientInputs->get('appid')->value();
265 $wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
266 if (!is_string($appId) || true !== $wasUsed) {
267 return $rpId;
268 }
269
270 return $appId;
271 }
272 }
273