1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | /** |
||||
6 | * @copyright Copyright (c) 2016, ownCloud, Inc. |
||||
7 | * |
||||
8 | * @author Christoph Wurst <[email protected]> |
||||
9 | * @author Joas Schilling <[email protected]> |
||||
10 | * @author Lukas Reschke <[email protected]> |
||||
11 | * @author Morris Jobke <[email protected]> |
||||
12 | * @author Roeland Jago Douma <[email protected]> |
||||
13 | * @author Victor Dubiniuk <[email protected]> |
||||
14 | * @author Vincent Petry <[email protected]> |
||||
15 | * @author Xheni Myrtaj <[email protected]> |
||||
16 | * |
||||
17 | * @license AGPL-3.0 |
||||
18 | * |
||||
19 | * This code is free software: you can redistribute it and/or modify |
||||
20 | * it under the terms of the GNU Affero General Public License, version 3, |
||||
21 | * as published by the Free Software Foundation. |
||||
22 | * |
||||
23 | * This program is distributed in the hope that it will be useful, |
||||
24 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
25 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
26 | * GNU Affero General Public License for more details. |
||||
27 | * |
||||
28 | * You should have received a copy of the GNU Affero General Public License, version 3, |
||||
29 | * along with this program. If not, see <http://www.gnu.org/licenses/> |
||||
30 | * |
||||
31 | */ |
||||
32 | namespace OC\IntegrityCheck; |
||||
33 | |||||
34 | use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder; |
||||
35 | use OC\IntegrityCheck\Exceptions\InvalidSignatureException; |
||||
36 | use OC\IntegrityCheck\Helpers\AppLocator; |
||||
37 | use OC\IntegrityCheck\Helpers\EnvironmentHelper; |
||||
38 | use OC\IntegrityCheck\Helpers\FileAccessHelper; |
||||
39 | use OC\IntegrityCheck\Iterator\ExcludeFileByNameFilterIterator; |
||||
40 | use OC\IntegrityCheck\Iterator\ExcludeFoldersByPathFilterIterator; |
||||
41 | use OCP\App\IAppManager; |
||||
42 | use OCP\Files\IMimeTypeDetector; |
||||
43 | use OCP\ICache; |
||||
44 | use OCP\ICacheFactory; |
||||
45 | use OCP\IConfig; |
||||
46 | use phpseclib\Crypt\RSA; |
||||
47 | use phpseclib\File\X509; |
||||
48 | |||||
49 | /** |
||||
50 | * Class Checker handles the code signing using X.509 and RSA. ownCloud ships with |
||||
51 | * a public root certificate certificate that allows to issue new certificates that |
||||
52 | * will be trusted for signing code. The CN will be used to verify that a certificate |
||||
53 | * given to a third-party developer may not be used for other applications. For |
||||
54 | * example the author of the application "calendar" would only receive a certificate |
||||
55 | * only valid for this application. |
||||
56 | * |
||||
57 | * @package OC\IntegrityCheck |
||||
58 | */ |
||||
59 | class Checker { |
||||
60 | public const CACHE_KEY = 'oc.integritycheck.checker'; |
||||
61 | /** @var EnvironmentHelper */ |
||||
62 | private $environmentHelper; |
||||
63 | /** @var AppLocator */ |
||||
64 | private $appLocator; |
||||
65 | /** @var FileAccessHelper */ |
||||
66 | private $fileAccessHelper; |
||||
67 | /** @var IConfig|null */ |
||||
68 | private $config; |
||||
69 | /** @var ICache */ |
||||
70 | private $cache; |
||||
71 | /** @var IAppManager|null */ |
||||
72 | private $appManager; |
||||
73 | /** @var IMimeTypeDetector */ |
||||
74 | private $mimeTypeDetector; |
||||
75 | |||||
76 | /** |
||||
77 | * @param EnvironmentHelper $environmentHelper |
||||
78 | * @param FileAccessHelper $fileAccessHelper |
||||
79 | * @param AppLocator $appLocator |
||||
80 | * @param IConfig|null $config |
||||
81 | * @param ICacheFactory $cacheFactory |
||||
82 | * @param IAppManager|null $appManager |
||||
83 | * @param IMimeTypeDetector $mimeTypeDetector |
||||
84 | */ |
||||
85 | public function __construct(EnvironmentHelper $environmentHelper, |
||||
86 | FileAccessHelper $fileAccessHelper, |
||||
87 | AppLocator $appLocator, |
||||
88 | ?IConfig $config, |
||||
89 | ICacheFactory $cacheFactory, |
||||
90 | ?IAppManager $appManager, |
||||
91 | IMimeTypeDetector $mimeTypeDetector) { |
||||
92 | $this->environmentHelper = $environmentHelper; |
||||
93 | $this->fileAccessHelper = $fileAccessHelper; |
||||
94 | $this->appLocator = $appLocator; |
||||
95 | $this->config = $config; |
||||
96 | $this->cache = $cacheFactory->createDistributed(self::CACHE_KEY); |
||||
97 | $this->appManager = $appManager; |
||||
98 | $this->mimeTypeDetector = $mimeTypeDetector; |
||||
99 | } |
||||
100 | |||||
101 | /** |
||||
102 | * Whether code signing is enforced or not. |
||||
103 | * |
||||
104 | * @return bool |
||||
105 | */ |
||||
106 | public function isCodeCheckEnforced(): bool { |
||||
107 | $notSignedChannels = [ '', 'git']; |
||||
108 | if (\in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) { |
||||
109 | return false; |
||||
110 | } |
||||
111 | |||||
112 | /** |
||||
113 | * This config option is undocumented and supposed to be so, it's only |
||||
114 | * applicable for very specific scenarios and we should not advertise it |
||||
115 | * too prominent. So please do not add it to config.sample.php. |
||||
116 | */ |
||||
117 | $isIntegrityCheckDisabled = false; |
||||
118 | if ($this->config !== null) { |
||||
119 | $isIntegrityCheckDisabled = $this->config->getSystemValueBool('integrity.check.disabled', false); |
||||
120 | } |
||||
121 | if ($isIntegrityCheckDisabled) { |
||||
122 | return false; |
||||
123 | } |
||||
124 | |||||
125 | return true; |
||||
126 | } |
||||
127 | |||||
128 | /** |
||||
129 | * Enumerates all files belonging to the folder. Sensible defaults are excluded. |
||||
130 | * |
||||
131 | * @param string $folderToIterate |
||||
132 | * @param string $root |
||||
133 | * @return \RecursiveIteratorIterator |
||||
134 | * @throws \Exception |
||||
135 | */ |
||||
136 | private function getFolderIterator(string $folderToIterate, string $root = ''): \RecursiveIteratorIterator { |
||||
137 | $dirItr = new \RecursiveDirectoryIterator( |
||||
138 | $folderToIterate, |
||||
139 | \RecursiveDirectoryIterator::SKIP_DOTS |
||||
140 | ); |
||||
141 | if ($root === '') { |
||||
142 | $root = \OC::$SERVERROOT; |
||||
143 | } |
||||
144 | $root = rtrim($root, '/'); |
||||
145 | |||||
146 | $excludeGenericFilesIterator = new ExcludeFileByNameFilterIterator($dirItr); |
||||
147 | $excludeFoldersIterator = new ExcludeFoldersByPathFilterIterator($excludeGenericFilesIterator, $root); |
||||
148 | |||||
149 | return new \RecursiveIteratorIterator( |
||||
150 | $excludeFoldersIterator, |
||||
151 | \RecursiveIteratorIterator::SELF_FIRST |
||||
152 | ); |
||||
153 | } |
||||
154 | |||||
155 | /** |
||||
156 | * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found |
||||
157 | * in the iterator. |
||||
158 | * |
||||
159 | * @param \RecursiveIteratorIterator $iterator |
||||
160 | * @param string $path |
||||
161 | * @return array Array of hashes. |
||||
162 | */ |
||||
163 | private function generateHashes(\RecursiveIteratorIterator $iterator, |
||||
164 | string $path): array { |
||||
165 | $hashes = []; |
||||
166 | |||||
167 | $baseDirectoryLength = \strlen($path); |
||||
168 | foreach ($iterator as $filename => $data) { |
||||
169 | /** @var \DirectoryIterator $data */ |
||||
170 | if ($data->isDir()) { |
||||
171 | continue; |
||||
172 | } |
||||
173 | |||||
174 | $relativeFileName = substr($filename, $baseDirectoryLength); |
||||
175 | $relativeFileName = ltrim($relativeFileName, '/'); |
||||
176 | |||||
177 | // Exclude signature.json files in the appinfo and root folder |
||||
178 | if ($relativeFileName === 'appinfo/signature.json') { |
||||
179 | continue; |
||||
180 | } |
||||
181 | // Exclude signature.json files in the appinfo and core folder |
||||
182 | if ($relativeFileName === 'core/signature.json') { |
||||
183 | continue; |
||||
184 | } |
||||
185 | |||||
186 | // The .htaccess file in the root folder of ownCloud can contain |
||||
187 | // custom content after the installation due to the fact that dynamic |
||||
188 | // content is written into it at installation time as well. This |
||||
189 | // includes for example the 404 and 403 instructions. |
||||
190 | // Thus we ignore everything below the first occurrence of |
||||
191 | // "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the |
||||
192 | // hash generated based on this. |
||||
193 | if ($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') { |
||||
194 | $fileContent = file_get_contents($filename); |
||||
195 | $explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent); |
||||
196 | if (\count($explodedArray) === 2) { |
||||
197 | $hashes[$relativeFileName] = hash('sha512', $explodedArray[0]); |
||||
198 | continue; |
||||
199 | } |
||||
200 | } |
||||
201 | if ($filename === $this->environmentHelper->getServerRoot() . '/core/js/mimetypelist.js') { |
||||
202 | $oldMimetypeList = new GenerateMimetypeFileBuilder(); |
||||
203 | $newFile = $oldMimetypeList->generateFile($this->mimeTypeDetector->getAllAliases()); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
204 | $oldFile = $this->fileAccessHelper->file_get_contents($filename); |
||||
205 | if ($newFile === $oldFile) { |
||||
206 | $hashes[$relativeFileName] = hash('sha512', $oldMimetypeList->generateFile($this->mimeTypeDetector->getOnlyDefaultAliases())); |
||||
0 ignored issues
–
show
The method
getOnlyDefaultAliases() does not exist on OCP\Files\IMimeTypeDetector . Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\Files\IMimeTypeDetector .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
207 | continue; |
||||
208 | } |
||||
209 | } |
||||
210 | |||||
211 | $hashes[$relativeFileName] = hash_file('sha512', $filename); |
||||
212 | } |
||||
213 | |||||
214 | return $hashes; |
||||
215 | } |
||||
216 | |||||
217 | /** |
||||
218 | * Creates the signature data |
||||
219 | * |
||||
220 | * @param array $hashes |
||||
221 | * @param X509 $certificate |
||||
222 | * @param RSA $privateKey |
||||
223 | * @return array |
||||
224 | */ |
||||
225 | private function createSignatureData(array $hashes, |
||||
226 | X509 $certificate, |
||||
227 | RSA $privateKey): array { |
||||
228 | ksort($hashes); |
||||
229 | |||||
230 | $privateKey->setSignatureMode(RSA::SIGNATURE_PSS); |
||||
231 | $privateKey->setMGFHash('sha512'); |
||||
232 | // See https://tools.ietf.org/html/rfc3447#page-38 |
||||
233 | $privateKey->setSaltLength(0); |
||||
234 | $signature = $privateKey->sign(json_encode($hashes)); |
||||
235 | |||||
236 | return [ |
||||
237 | 'hashes' => $hashes, |
||||
238 | 'signature' => base64_encode($signature), |
||||
239 | 'certificate' => $certificate->saveX509($certificate->currentCert), |
||||
240 | ]; |
||||
241 | } |
||||
242 | |||||
243 | /** |
||||
244 | * Write the signature of the app in the specified folder |
||||
245 | * |
||||
246 | * @param string $path |
||||
247 | * @param X509 $certificate |
||||
248 | * @param RSA $privateKey |
||||
249 | * @throws \Exception |
||||
250 | */ |
||||
251 | public function writeAppSignature($path, |
||||
252 | X509 $certificate, |
||||
253 | RSA $privateKey) { |
||||
254 | $appInfoDir = $path . '/appinfo'; |
||||
255 | try { |
||||
256 | $this->fileAccessHelper->assertDirectoryExists($appInfoDir); |
||||
257 | |||||
258 | $iterator = $this->getFolderIterator($path); |
||||
259 | $hashes = $this->generateHashes($iterator, $path); |
||||
260 | $signature = $this->createSignatureData($hashes, $certificate, $privateKey); |
||||
261 | $this->fileAccessHelper->file_put_contents( |
||||
262 | $appInfoDir . '/signature.json', |
||||
263 | json_encode($signature, JSON_PRETTY_PRINT) |
||||
264 | ); |
||||
265 | } catch (\Exception $e) { |
||||
266 | if (!$this->fileAccessHelper->is_writable($appInfoDir)) { |
||||
267 | throw new \Exception($appInfoDir . ' is not writable'); |
||||
268 | } |
||||
269 | throw $e; |
||||
270 | } |
||||
271 | } |
||||
272 | |||||
273 | /** |
||||
274 | * Write the signature of core |
||||
275 | * |
||||
276 | * @param X509 $certificate |
||||
277 | * @param RSA $rsa |
||||
278 | * @param string $path |
||||
279 | * @throws \Exception |
||||
280 | */ |
||||
281 | public function writeCoreSignature(X509 $certificate, |
||||
282 | RSA $rsa, |
||||
283 | $path) { |
||||
284 | $coreDir = $path . '/core'; |
||||
285 | try { |
||||
286 | $this->fileAccessHelper->assertDirectoryExists($coreDir); |
||||
287 | $iterator = $this->getFolderIterator($path, $path); |
||||
288 | $hashes = $this->generateHashes($iterator, $path); |
||||
289 | $signatureData = $this->createSignatureData($hashes, $certificate, $rsa); |
||||
290 | $this->fileAccessHelper->file_put_contents( |
||||
291 | $coreDir . '/signature.json', |
||||
292 | json_encode($signatureData, JSON_PRETTY_PRINT) |
||||
293 | ); |
||||
294 | } catch (\Exception $e) { |
||||
295 | if (!$this->fileAccessHelper->is_writable($coreDir)) { |
||||
296 | throw new \Exception($coreDir . ' is not writable'); |
||||
297 | } |
||||
298 | throw $e; |
||||
299 | } |
||||
300 | } |
||||
301 | |||||
302 | /** |
||||
303 | * Split the certificate file in individual certs |
||||
304 | * |
||||
305 | * @param string $cert |
||||
306 | * @return string[] |
||||
307 | */ |
||||
308 | private function splitCerts(string $cert): array { |
||||
309 | preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches); |
||||
310 | |||||
311 | return $matches[0]; |
||||
312 | } |
||||
313 | |||||
314 | /** |
||||
315 | * Verifies the signature for the specified path. |
||||
316 | * |
||||
317 | * @param string $signaturePath |
||||
318 | * @param string $basePath |
||||
319 | * @param string $certificateCN |
||||
320 | * @param bool $forceVerify |
||||
321 | * @return array |
||||
322 | * @throws InvalidSignatureException |
||||
323 | * @throws \Exception |
||||
324 | */ |
||||
325 | private function verify(string $signaturePath, string $basePath, string $certificateCN, bool $forceVerify = false): array { |
||||
326 | if (!$forceVerify && !$this->isCodeCheckEnforced()) { |
||||
327 | return []; |
||||
328 | } |
||||
329 | |||||
330 | $content = $this->fileAccessHelper->file_get_contents($signaturePath); |
||||
331 | $signatureData = null; |
||||
332 | |||||
333 | if (\is_string($content)) { |
||||
334 | $signatureData = json_decode($content, true); |
||||
335 | } |
||||
336 | if (!\is_array($signatureData)) { |
||||
337 | throw new InvalidSignatureException('Signature data not found.'); |
||||
338 | } |
||||
339 | |||||
340 | $expectedHashes = $signatureData['hashes']; |
||||
341 | ksort($expectedHashes); |
||||
342 | $signature = base64_decode($signatureData['signature']); |
||||
343 | $certificate = $signatureData['certificate']; |
||||
344 | |||||
345 | // Check if certificate is signed by Nextcloud Root Authority |
||||
346 | $x509 = new \phpseclib\File\X509(); |
||||
347 | $rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt'); |
||||
348 | |||||
349 | $rootCerts = $this->splitCerts($rootCertificatePublicKey); |
||||
350 | foreach ($rootCerts as $rootCert) { |
||||
351 | $x509->loadCA($rootCert); |
||||
352 | } |
||||
353 | $x509->loadX509($certificate); |
||||
354 | if (!$x509->validateSignature()) { |
||||
355 | throw new InvalidSignatureException('Certificate is not valid.'); |
||||
356 | } |
||||
357 | // Verify if certificate has proper CN. "core" CN is always trusted. |
||||
358 | if ($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') { |
||||
359 | throw new InvalidSignatureException( |
||||
360 | sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN']) |
||||
361 | ); |
||||
362 | } |
||||
363 | |||||
364 | // Check if the signature of the files is valid |
||||
365 | $rsa = new \phpseclib\Crypt\RSA(); |
||||
366 | $rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']); |
||||
367 | $rsa->setSignatureMode(RSA::SIGNATURE_PSS); |
||||
368 | $rsa->setMGFHash('sha512'); |
||||
369 | // See https://tools.ietf.org/html/rfc3447#page-38 |
||||
370 | $rsa->setSaltLength(0); |
||||
371 | if (!$rsa->verify(json_encode($expectedHashes), $signature)) { |
||||
372 | throw new InvalidSignatureException('Signature could not get verified.'); |
||||
373 | } |
||||
374 | |||||
375 | // Fixes for the updater as shipped with ownCloud 9.0.x: The updater is |
||||
376 | // replaced after the code integrity check is performed. |
||||
377 | // |
||||
378 | // Due to this reason we exclude the whole updater/ folder from the code |
||||
379 | // integrity check. |
||||
380 | if ($basePath === $this->environmentHelper->getServerRoot()) { |
||||
381 | foreach ($expectedHashes as $fileName => $hash) { |
||||
382 | if (str_starts_with($fileName, 'updater/')) { |
||||
383 | unset($expectedHashes[$fileName]); |
||||
384 | } |
||||
385 | } |
||||
386 | } |
||||
387 | |||||
388 | // Compare the list of files which are not identical |
||||
389 | $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath); |
||||
390 | $differencesA = array_diff($expectedHashes, $currentInstanceHashes); |
||||
391 | $differencesB = array_diff($currentInstanceHashes, $expectedHashes); |
||||
392 | $differences = array_unique(array_merge($differencesA, $differencesB)); |
||||
393 | $differenceArray = []; |
||||
394 | foreach ($differences as $filename => $hash) { |
||||
395 | // Check if file should not exist in the new signature table |
||||
396 | if (!array_key_exists($filename, $expectedHashes)) { |
||||
397 | $differenceArray['EXTRA_FILE'][$filename]['expected'] = ''; |
||||
398 | $differenceArray['EXTRA_FILE'][$filename]['current'] = $hash; |
||||
399 | continue; |
||||
400 | } |
||||
401 | |||||
402 | // Check if file is missing |
||||
403 | if (!array_key_exists($filename, $currentInstanceHashes)) { |
||||
404 | $differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename]; |
||||
405 | $differenceArray['FILE_MISSING'][$filename]['current'] = ''; |
||||
406 | continue; |
||||
407 | } |
||||
408 | |||||
409 | // Check if hash does mismatch |
||||
410 | if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) { |
||||
411 | $differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename]; |
||||
412 | $differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename]; |
||||
413 | continue; |
||||
414 | } |
||||
415 | |||||
416 | // Should never happen. |
||||
417 | throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.'); |
||||
418 | } |
||||
419 | |||||
420 | return $differenceArray; |
||||
421 | } |
||||
422 | |||||
423 | /** |
||||
424 | * Whether the code integrity check has passed successful or not |
||||
425 | * |
||||
426 | * @return bool |
||||
427 | */ |
||||
428 | public function hasPassedCheck(): bool { |
||||
429 | $results = $this->getResults(); |
||||
430 | if (empty($results)) { |
||||
431 | return true; |
||||
432 | } |
||||
433 | |||||
434 | return false; |
||||
435 | } |
||||
436 | |||||
437 | /** |
||||
438 | * @return array |
||||
439 | */ |
||||
440 | public function getResults(): array { |
||||
441 | $cachedResults = $this->cache->get(self::CACHE_KEY); |
||||
442 | if (!\is_null($cachedResults) and $cachedResults !== false) { |
||||
443 | return json_decode($cachedResults, true); |
||||
444 | } |
||||
445 | |||||
446 | if ($this->config !== null) { |
||||
447 | return json_decode($this->config->getAppValue('core', self::CACHE_KEY, '{}'), true); |
||||
448 | } |
||||
449 | return []; |
||||
450 | } |
||||
451 | |||||
452 | /** |
||||
453 | * Stores the results in the app config as well as cache |
||||
454 | * |
||||
455 | * @param string $scope |
||||
456 | * @param array $result |
||||
457 | */ |
||||
458 | private function storeResults(string $scope, array $result) { |
||||
459 | $resultArray = $this->getResults(); |
||||
460 | unset($resultArray[$scope]); |
||||
461 | if (!empty($result)) { |
||||
462 | $resultArray[$scope] = $result; |
||||
463 | } |
||||
464 | if ($this->config !== null) { |
||||
465 | $this->config->setAppValue('core', self::CACHE_KEY, json_encode($resultArray)); |
||||
466 | } |
||||
467 | $this->cache->set(self::CACHE_KEY, json_encode($resultArray)); |
||||
468 | } |
||||
469 | |||||
470 | /** |
||||
471 | * |
||||
472 | * Clean previous results for a proper rescanning. Otherwise |
||||
473 | */ |
||||
474 | private function cleanResults() { |
||||
475 | $this->config->deleteAppValue('core', self::CACHE_KEY); |
||||
476 | $this->cache->remove(self::CACHE_KEY); |
||||
477 | } |
||||
478 | |||||
479 | /** |
||||
480 | * Verify the signature of $appId. Returns an array with the following content: |
||||
481 | * [ |
||||
482 | * 'FILE_MISSING' => |
||||
483 | * [ |
||||
484 | * 'filename' => [ |
||||
485 | * 'expected' => 'expectedSHA512', |
||||
486 | * 'current' => 'currentSHA512', |
||||
487 | * ], |
||||
488 | * ], |
||||
489 | * 'EXTRA_FILE' => |
||||
490 | * [ |
||||
491 | * 'filename' => [ |
||||
492 | * 'expected' => 'expectedSHA512', |
||||
493 | * 'current' => 'currentSHA512', |
||||
494 | * ], |
||||
495 | * ], |
||||
496 | * 'INVALID_HASH' => |
||||
497 | * [ |
||||
498 | * 'filename' => [ |
||||
499 | * 'expected' => 'expectedSHA512', |
||||
500 | * 'current' => 'currentSHA512', |
||||
501 | * ], |
||||
502 | * ], |
||||
503 | * ] |
||||
504 | * |
||||
505 | * Array may be empty in case no problems have been found. |
||||
506 | * |
||||
507 | * @param string $appId |
||||
508 | * @param string $path Optional path. If none is given it will be guessed. |
||||
509 | * @param bool $forceVerify |
||||
510 | * @return array |
||||
511 | */ |
||||
512 | public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array { |
||||
513 | try { |
||||
514 | if ($path === '') { |
||||
515 | $path = $this->appLocator->getAppPath($appId); |
||||
516 | } |
||||
517 | $result = $this->verify( |
||||
518 | $path . '/appinfo/signature.json', |
||||
519 | $path, |
||||
520 | $appId, |
||||
521 | $forceVerify |
||||
522 | ); |
||||
523 | } catch (\Exception $e) { |
||||
524 | $result = [ |
||||
525 | 'EXCEPTION' => [ |
||||
526 | 'class' => \get_class($e), |
||||
527 | 'message' => $e->getMessage(), |
||||
528 | ], |
||||
529 | ]; |
||||
530 | } |
||||
531 | $this->storeResults($appId, $result); |
||||
532 | |||||
533 | return $result; |
||||
534 | } |
||||
535 | |||||
536 | /** |
||||
537 | * Verify the signature of core. Returns an array with the following content: |
||||
538 | * [ |
||||
539 | * 'FILE_MISSING' => |
||||
540 | * [ |
||||
541 | * 'filename' => [ |
||||
542 | * 'expected' => 'expectedSHA512', |
||||
543 | * 'current' => 'currentSHA512', |
||||
544 | * ], |
||||
545 | * ], |
||||
546 | * 'EXTRA_FILE' => |
||||
547 | * [ |
||||
548 | * 'filename' => [ |
||||
549 | * 'expected' => 'expectedSHA512', |
||||
550 | * 'current' => 'currentSHA512', |
||||
551 | * ], |
||||
552 | * ], |
||||
553 | * 'INVALID_HASH' => |
||||
554 | * [ |
||||
555 | * 'filename' => [ |
||||
556 | * 'expected' => 'expectedSHA512', |
||||
557 | * 'current' => 'currentSHA512', |
||||
558 | * ], |
||||
559 | * ], |
||||
560 | * ] |
||||
561 | * |
||||
562 | * Array may be empty in case no problems have been found. |
||||
563 | * |
||||
564 | * @return array |
||||
565 | */ |
||||
566 | public function verifyCoreSignature(): array { |
||||
567 | try { |
||||
568 | $result = $this->verify( |
||||
569 | $this->environmentHelper->getServerRoot() . '/core/signature.json', |
||||
570 | $this->environmentHelper->getServerRoot(), |
||||
571 | 'core' |
||||
572 | ); |
||||
573 | } catch (\Exception $e) { |
||||
574 | $result = [ |
||||
575 | 'EXCEPTION' => [ |
||||
576 | 'class' => \get_class($e), |
||||
577 | 'message' => $e->getMessage(), |
||||
578 | ], |
||||
579 | ]; |
||||
580 | } |
||||
581 | $this->storeResults('core', $result); |
||||
582 | |||||
583 | return $result; |
||||
584 | } |
||||
585 | |||||
586 | /** |
||||
587 | * Verify the core code of the instance as well as all applicable applications |
||||
588 | * and store the results. |
||||
589 | */ |
||||
590 | public function runInstanceVerification() { |
||||
591 | $this->cleanResults(); |
||||
592 | $this->verifyCoreSignature(); |
||||
593 | $appIds = $this->appLocator->getAllApps(); |
||||
594 | foreach ($appIds as $appId) { |
||||
595 | // If an application is shipped a valid signature is required |
||||
596 | $isShipped = $this->appManager->isShipped($appId); |
||||
597 | $appNeedsToBeChecked = false; |
||||
598 | if ($isShipped) { |
||||
599 | $appNeedsToBeChecked = true; |
||||
600 | } elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) { |
||||
601 | // Otherwise only if the application explicitly ships a signature.json file |
||||
602 | $appNeedsToBeChecked = true; |
||||
603 | } |
||||
604 | |||||
605 | if ($appNeedsToBeChecked) { |
||||
606 | $this->verifyAppSignature($appId); |
||||
607 | } |
||||
608 | } |
||||
609 | } |
||||
610 | } |
||||
611 |