PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.0
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 / metadata-service / src / MetadataService.php

MetadataService.php in Defender Security – Malware Scanner, Login Security & Firewall 3.1.0, at vendor/web-auth/metadata-service/src/MetadataService.php

284 lines 10.0 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\MetadataService;
15
16 use Assert\Assertion;
17 use Base64Url\Base64Url;
18 use function count;
19 use InvalidArgumentException;
20 use function is_array;
21 use Jose\Component\KeyManagement\JWKFactory;
22 use Jose\Component\Signature\Algorithm\ES256;
23 use Jose\Component\Signature\Serializer\CompactSerializer;
24 use League\Uri\UriString;
25 use LogicException;
26 use Psr\Http\Client\ClientInterface;
27 use Psr\Http\Message\RequestFactoryInterface;
28 use Psr\Log\LoggerInterface;
29 use Psr\Log\NullLogger;
30 use function Safe\json_decode;
31 use function Safe\sprintf;
32 use Throwable;
33 use Webauthn\CertificateToolbox;
34
35 class MetadataService
36 {
37 /**
38 * @var ClientInterface
39 */
40 private $httpClient;
41
42 /**
43 * @var RequestFactoryInterface
44 */
45 private $requestFactory;
46
47 /**
48 * @var array
49 */
50 private $additionalQueryStringValues;
51
52 /**
53 * @var array
54 */
55 private $additionalHeaders;
56
57 /**
58 * @var string
59 */
60 private $serviceUri;
61
62 /**
63 * @var LoggerInterface
64 */
65 private $logger;
66
67 public function __construct(string $serviceUri, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, array $additionalQueryStringValues = [], array $additionalHeaders = [], ?LoggerInterface $logger = null)
68 {
69 if (0 !== count($additionalQueryStringValues)) {
70 @trigger_error('The argument "additionalQueryStringValues" is deprecated since version 3.3 and will be removed in 4.0. Please set an empty array instead and us the method `addQueryStringValues`.', E_USER_DEPRECATED);
71 }
72 if (0 !== count($additionalQueryStringValues)) {
73 @trigger_error('The argument "additionalHeaders" is deprecated since version 3.3 and will be removed in 4.0. Please set an empty array instead and us the method `addHeaders`.', E_USER_DEPRECATED);
74 }
75 if (null !== $logger) {
76 @trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger" instead.', E_USER_DEPRECATED);
77 }
78 $this->serviceUri = $serviceUri;
79 $this->httpClient = $httpClient;
80 $this->requestFactory = $requestFactory;
81 $this->additionalQueryStringValues = $additionalQueryStringValues;
82 $this->additionalHeaders = $additionalHeaders;
83 $this->logger = $logger ?? new NullLogger();
84 }
85
86 public function addQueryStringValues(array $additionalQueryStringValues): self
87 {
88 $this->additionalQueryStringValues = $additionalQueryStringValues;
89
90 return $this;
91 }
92
93 public function addHeaders(array $additionalHeaders): self
94 {
95 $this->additionalHeaders = $additionalHeaders;
96
97 return $this;
98 }
99
100 public function setLogger(LoggerInterface $logger): self
101 {
102 $this->logger = $logger;
103
104 return $this;
105 }
106
107 public function has(string $aaguid): bool
108 {
109 try {
110 $toc = $this->fetchMetadataTOCPayload();
111 } catch (Throwable $e) {
112 return false;
113 }
114 foreach ($toc->getEntries() as $entry) {
115 if ($entry->getAaguid() === $aaguid && null !== $entry->getUrl()) {
116 return true;
117 }
118 }
119
120 return false;
121 }
122
123 public function get(string $aaguid): MetadataStatement
124 {
125 $toc = $this->fetchMetadataTOCPayload();
126 foreach ($toc->getEntries() as $entry) {
127 if ($entry->getAaguid() === $aaguid && null !== $entry->getUrl()) {
128 $mds = $this->fetchMetadataStatementFor($entry);
129 $mds
130 ->setStatusReports($entry->getStatusReports())
131 ->setRootCertificates($toc->getRootCertificates())
132 ;
133
134 return $mds;
135 }
136 }
137
138 throw new InvalidArgumentException(sprintf('The Metadata Statement with AAGUID "%s" is missing', $aaguid));
139 }
140
141 /**
142 * @deprecated This method is deprecated since v3.3 and will be removed in v4.0
143 */
144 public function getMetadataStatementFor(MetadataTOCPayloadEntry $entry, string $hashingFunction = 'sha256'): MetadataStatement
145 {
146 return $this->fetchMetadataStatementFor($entry, $hashingFunction);
147 }
148
149 public function fetchMetadataStatementFor(MetadataTOCPayloadEntry $entry, string $hashingFunction = 'sha256'): MetadataStatement
150 {
151 $this->logger->info('Trying to get the metadata statement for a given entry', ['entry' => $entry]);
152 try {
153 $hash = $entry->getHash();
154 $url = $entry->getUrl();
155 if (null === $hash || null === $url) {
156 throw new LogicException('The Metadata Statement has not been published');
157 }
158 $uri = $this->buildUri($url);
159 $result = $this->fetchMetadataStatement($uri, true, $hash, $hashingFunction);
160 $this->logger->info('The metadata statement exists');
161 $this->logger->debug('Metadata Statement', ['mds' => $result]);
162
163 return $result;
164 } catch (Throwable $throwable) {
165 $this->logger->error('An error occurred', [
166 'exception' => $throwable,
167 ]);
168 throw $throwable;
169 }
170 }
171
172 /**
173 * @deprecated This method is deprecated since v3.3 and will be removed in v4.0
174 */
175 public function getMetadataTOCPayload(): MetadataTOCPayload
176 {
177 return $this->fetchMetadataTOCPayload();
178 }
179
180 private function fetchMetadataTOCPayload(): MetadataTOCPayload
181 {
182 $this->logger->info('Trying to get the metadata service TOC payload');
183 try {
184 $uri = $this->buildUri($this->serviceUri);
185 $toc = $this->fetchTableOfContent($uri);
186 $this->logger->info('The TOC payload has been received');
187 $this->logger->debug('TOC payload', ['toc' => $toc]);
188
189 return $toc;
190 } catch (Throwable $throwable) {
191 $this->logger->error('An error occurred', [
192 'exception' => $throwable,
193 ]);
194 throw $throwable;
195 }
196 }
197
198 private function buildUri(string $uri): string
199 {
200 $parsedUri = UriString::parse($uri);
201 $queryString = $parsedUri['query'];
202 $query = [];
203 if (null !== $queryString) {
204 parse_str($queryString, $query);
205 }
206 foreach ($this->additionalQueryStringValues as $k => $v) {
207 if (!isset($query[$k])) {
208 $query[$k] = $v;
209 continue;
210 }
211 if (!is_array($query[$k])) {
212 $query[$k] = [$query[$k], $v];
213 continue;
214 }
215 $query[$k][] = $v;
216 }
217 $parsedUri['query'] = 0 === count($query) ? null : http_build_query($query, '', '&', PHP_QUERY_RFC3986);
218
219 return UriString::build($parsedUri);
220 }
221
222 private function fetchTableOfContent(string $uri): MetadataTOCPayload
223 {
224 $content = $this->fetch($uri);
225 $rootCertificates = [];
226 $payload = $this->getJwsPayload($content, $rootCertificates);
227 $data = json_decode($payload, true);
228
229 $toc = MetadataTOCPayload::createFromArray($data);
230 $toc->setRootCertificates($rootCertificates);
231
232 return $toc;
233 }
234
235 private function fetchMetadataStatement(string $uri, bool $isBase64UrlEncoded, string $hash = '', string $hashingFunction = 'sha256'): MetadataStatement
236 {
237 $payload = $this->fetch($uri);
238 if ('' !== $hash) {
239 Assertion::true(hash_equals($hash, hash($hashingFunction, $payload, true)), 'The hash cannot be verified. The metadata statement shall be rejected');
240 }
241 $json = $isBase64UrlEncoded ? Base64Url::decode($payload) : $payload;
242 $data = json_decode($json, true);
243
244 return MetadataStatement::createFromArray($data);
245 }
246
247 private function fetch(string $uri): string
248 {
249 $request = $this->requestFactory->createRequest('GET', $uri);
250 foreach ($this->additionalHeaders as $k => $v) {
251 $request = $request->withHeader($k, $v);
252 }
253 $response = $this->httpClient->sendRequest($request);
254 Assertion::eq(200, $response->getStatusCode(), sprintf('Unable to contact the server. Response code is %d', $response->getStatusCode()));
255 $content = $response->getBody()->getContents();
256 Assertion::notEmpty($content, 'Unable to contact the server. The response has no content');
257
258 return $content;
259 }
260
261 private function getJwsPayload(string $token, array &$rootCertificates): string
262 {
263 $jws = (new CompactSerializer())->unserialize($token);
264 Assertion::eq(1, $jws->countSignatures(), 'Invalid response from the metadata service. Only one signature shall be present.');
265 $signature = $jws->getSignature(0);
266 $payload = $jws->getPayload();
267 Assertion::notEmpty($payload, 'Invalid response from the metadata service. The token payload is empty.');
268 $header = $signature->getProtectedHeader();
269 Assertion::keyExists($header, 'alg', 'The "alg" parameter is missing.');
270 Assertion::eq($header['alg'], 'ES256', 'The expected "alg" parameter value should be "ES256".');
271 Assertion::keyExists($header, 'x5c', 'The "x5c" parameter is missing.');
272 Assertion::isArray($header['x5c'], 'The "x5c" parameter should be an array.');
273 $key = JWKFactory::createFromX5C($header['x5c']);
274 $rootCertificates = array_map(static function (string $x509): string {
275 return CertificateToolbox::fixPEMStructure($x509);
276 }, $header['x5c']);
277 $algorithm = new ES256();
278 $isValid = $algorithm->verify($key, $signature->getEncodedProtectedHeader().'.'.$jws->getEncodedPayload(), $signature->getSignature());
279 Assertion::true($isValid, 'Invalid response from the metadata service. The token signature is invalid.');
280
281 return $jws->getPayload();
282 }
283 }
284