Passed
Push — master ( 91864a...0893bb )
by Roeland
24:06 queued 12s
created

Checker::storeResults()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 4
nop 2
dl 0
loc 10
rs 10
c 0
b 0
f 0
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
33
namespace OC\IntegrityCheck;
34
35
use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder;
36
use OC\IntegrityCheck\Exceptions\InvalidSignatureException;
37
use OC\IntegrityCheck\Helpers\AppLocator;
38
use OC\IntegrityCheck\Helpers\EnvironmentHelper;
39
use OC\IntegrityCheck\Helpers\FileAccessHelper;
40
use OC\IntegrityCheck\Iterator\ExcludeFileByNameFilterIterator;
41
use OC\IntegrityCheck\Iterator\ExcludeFoldersByPathFilterIterator;
42
use OCP\App\IAppManager;
43
use OCP\Files\IMimeTypeDetector;
44
use OCP\ICache;
45
use OCP\ICacheFactory;
46
use OCP\IConfig;
47
use phpseclib\Crypt\RSA;
48
use phpseclib\File\X509;
49
50
/**
51
 * Class Checker handles the code signing using X.509 and RSA. ownCloud ships with
52
 * a public root certificate certificate that allows to issue new certificates that
53
 * will be trusted for signing code. The CN will be used to verify that a certificate
54
 * given to a third-party developer may not be used for other applications. For
55
 * example the author of the application "calendar" would only receive a certificate
56
 * only valid for this application.
57
 *
58
 * @package OC\IntegrityCheck
59
 */
60
class Checker {
61
	public const CACHE_KEY = 'oc.integritycheck.checker';
62
	/** @var EnvironmentHelper */
63
	private $environmentHelper;
64
	/** @var AppLocator */
65
	private $appLocator;
66
	/** @var FileAccessHelper */
67
	private $fileAccessHelper;
68
	/** @var IConfig|null */
69
	private $config;
70
	/** @var ICache */
71
	private $cache;
72
	/** @var IAppManager|null */
73
	private $appManager;
74
	/** @var IMimeTypeDetector */
75
	private $mimeTypeDetector;
76
77
	/**
78
	 * @param EnvironmentHelper $environmentHelper
79
	 * @param FileAccessHelper $fileAccessHelper
80
	 * @param AppLocator $appLocator
81
	 * @param IConfig|null $config
82
	 * @param ICacheFactory $cacheFactory
83
	 * @param IAppManager|null $appManager
84
	 * @param IMimeTypeDetector $mimeTypeDetector
85
	 */
86
	public function __construct(EnvironmentHelper $environmentHelper,
87
								FileAccessHelper $fileAccessHelper,
88
								AppLocator $appLocator,
89
								?IConfig $config,
90
								ICacheFactory $cacheFactory,
91
								?IAppManager $appManager,
92
								IMimeTypeDetector $mimeTypeDetector) {
93
		$this->environmentHelper = $environmentHelper;
94
		$this->fileAccessHelper = $fileAccessHelper;
95
		$this->appLocator = $appLocator;
96
		$this->config = $config;
97
		$this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
98
		$this->appManager = $appManager;
99
		$this->mimeTypeDetector = $mimeTypeDetector;
100
	}
101
102
	/**
103
	 * Whether code signing is enforced or not.
104
	 *
105
	 * @return bool
106
	 */
107
	public function isCodeCheckEnforced(): bool {
108
		$notSignedChannels = [ '', 'git'];
109
		if (\in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) {
110
			return false;
111
		}
112
113
		/**
114
		 * This config option is undocumented and supposed to be so, it's only
115
		 * applicable for very specific scenarios and we should not advertise it
116
		 * too prominent. So please do not add it to config.sample.php.
117
		 */
118
		$isIntegrityCheckDisabled = false;
119
		if ($this->config !== null) {
120
			$isIntegrityCheckDisabled = $this->config->getSystemValue('integrity.check.disabled', false);
121
		}
122
		if ($isIntegrityCheckDisabled === true) {
123
			return false;
124
		}
125
126
		return true;
127
	}
128
129
	/**
130
	 * Enumerates all files belonging to the folder. Sensible defaults are excluded.
131
	 *
132
	 * @param string $folderToIterate
133
	 * @param string $root
134
	 * @return \RecursiveIteratorIterator
135
	 * @throws \Exception
136
	 */
137
	private function getFolderIterator(string $folderToIterate, string $root = ''): \RecursiveIteratorIterator {
138
		$dirItr = new \RecursiveDirectoryIterator(
139
			$folderToIterate,
140
			\RecursiveDirectoryIterator::SKIP_DOTS
141
		);
142
		if ($root === '') {
143
			$root = \OC::$SERVERROOT;
144
		}
145
		$root = rtrim($root, '/');
146
147
		$excludeGenericFilesIterator = new ExcludeFileByNameFilterIterator($dirItr);
148
		$excludeFoldersIterator = new ExcludeFoldersByPathFilterIterator($excludeGenericFilesIterator, $root);
149
150
		return new \RecursiveIteratorIterator(
151
			$excludeFoldersIterator,
152
			\RecursiveIteratorIterator::SELF_FIRST
153
		);
154
	}
155
156
	/**
157
	 * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found
158
	 * in the iterator.
159
	 *
160
	 * @param \RecursiveIteratorIterator $iterator
161
	 * @param string $path
162
	 * @return array Array of hashes.
163
	 */
164
	private function generateHashes(\RecursiveIteratorIterator $iterator,
165
									string $path): array {
166
		$hashes = [];
167
168
		$baseDirectoryLength = \strlen($path);
169
		foreach ($iterator as $filename => $data) {
170
			/** @var \DirectoryIterator $data */
171
			if ($data->isDir()) {
172
				continue;
173
			}
174
175
			$relativeFileName = substr($filename, $baseDirectoryLength);
176
			$relativeFileName = ltrim($relativeFileName, '/');
177
178
			// Exclude signature.json files in the appinfo and root folder
179
			if ($relativeFileName === 'appinfo/signature.json') {
180
				continue;
181
			}
182
			// Exclude signature.json files in the appinfo and core folder
183
			if ($relativeFileName === 'core/signature.json') {
184
				continue;
185
			}
186
187
			// The .htaccess file in the root folder of ownCloud can contain
188
			// custom content after the installation due to the fact that dynamic
189
			// content is written into it at installation time as well. This
190
			// includes for example the 404 and 403 instructions.
191
			// Thus we ignore everything below the first occurrence of
192
			// "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the
193
			// hash generated based on this.
194
			if ($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') {
195
				$fileContent = file_get_contents($filename);
196
				$explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent);
197
				if (\count($explodedArray) === 2) {
198
					$hashes[$relativeFileName] = hash('sha512', $explodedArray[0]);
199
					continue;
200
				}
201
			}
202
			if ($filename === $this->environmentHelper->getServerRoot() . '/core/js/mimetypelist.js') {
203
				$oldMimetypeList = new GenerateMimetypeFileBuilder();
204
				$newFile = $oldMimetypeList->generateFile($this->mimeTypeDetector->getAllAliases());
0 ignored issues
show
Bug introduced by
The method getAllAliases() 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 ignore-call  annotation

204
				$newFile = $oldMimetypeList->generateFile($this->mimeTypeDetector->/** @scrutinizer ignore-call */ getAllAliases());
Loading history...
205
				if ($newFile === file_get_contents($filename)) {
206
					$hashes[$relativeFileName] = hash('sha512', $oldMimetypeList->generateFile($this->mimeTypeDetector->getOnlyDefaultAliases()));
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

206
					$hashes[$relativeFileName] = hash('sha512', $oldMimetypeList->generateFile($this->mimeTypeDetector->/** @scrutinizer ignore-call */ getOnlyDefaultAliases()));
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
	 * Verifies the signature for the specified path.
304
	 *
305
	 * @param string $signaturePath
306
	 * @param string $basePath
307
	 * @param string $certificateCN
308
	 * @param bool $forceVerify
309
	 * @return array
310
	 * @throws InvalidSignatureException
311
	 * @throws \Exception
312
	 */
313
	private function verify(string $signaturePath, string $basePath, string $certificateCN, bool $forceVerify = false): array {
314
		if (!$forceVerify && !$this->isCodeCheckEnforced()) {
315
			return [];
316
		}
317
318
		$content = $this->fileAccessHelper->file_get_contents($signaturePath);
319
		$signatureData = null;
320
321
		if (\is_string($content)) {
0 ignored issues
show
introduced by
The condition is_string($content) is always true.
Loading history...
322
			$signatureData = json_decode($content, true);
323
		}
324
		if (!\is_array($signatureData)) {
325
			throw new InvalidSignatureException('Signature data not found.');
326
		}
327
328
		$expectedHashes = $signatureData['hashes'];
329
		ksort($expectedHashes);
330
		$signature = base64_decode($signatureData['signature']);
331
		$certificate = $signatureData['certificate'];
332
333
		// Check if certificate is signed by Nextcloud Root Authority
334
		$x509 = new \phpseclib\File\X509();
335
		$rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
336
		$x509->loadCA($rootCertificatePublicKey);
337
		$x509->loadX509($certificate);
338
		if (!$x509->validateSignature()) {
339
			throw new InvalidSignatureException('Certificate is not valid.');
340
		}
341
		// Verify if certificate has proper CN. "core" CN is always trusted.
342
		if ($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
343
			throw new InvalidSignatureException(
344
					sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
345
			);
346
		}
347
348
		// Check if the signature of the files is valid
349
		$rsa = new \phpseclib\Crypt\RSA();
350
		$rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
351
		$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
352
		$rsa->setMGFHash('sha512');
353
		// See https://tools.ietf.org/html/rfc3447#page-38
354
		$rsa->setSaltLength(0);
355
		if (!$rsa->verify(json_encode($expectedHashes), $signature)) {
356
			throw new InvalidSignatureException('Signature could not get verified.');
357
		}
358
359
		// Fixes for the updater as shipped with ownCloud 9.0.x: The updater is
360
		// replaced after the code integrity check is performed.
361
		//
362
		// Due to this reason we exclude the whole updater/ folder from the code
363
		// integrity check.
364
		if ($basePath === $this->environmentHelper->getServerRoot()) {
365
			foreach ($expectedHashes as $fileName => $hash) {
366
				if (strpos($fileName, 'updater/') === 0) {
367
					unset($expectedHashes[$fileName]);
368
				}
369
			}
370
		}
371
372
		// Compare the list of files which are not identical
373
		$currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath);
374
		$differencesA = array_diff($expectedHashes, $currentInstanceHashes);
375
		$differencesB = array_diff($currentInstanceHashes, $expectedHashes);
376
		$differences = array_unique(array_merge($differencesA, $differencesB));
377
		$differenceArray = [];
378
		foreach ($differences as $filename => $hash) {
379
			// Check if file should not exist in the new signature table
380
			if (!array_key_exists($filename, $expectedHashes)) {
381
				$differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
382
				$differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
383
				continue;
384
			}
385
386
			// Check if file is missing
387
			if (!array_key_exists($filename, $currentInstanceHashes)) {
388
				$differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
389
				$differenceArray['FILE_MISSING'][$filename]['current'] = '';
390
				continue;
391
			}
392
393
			// Check if hash does mismatch
394
			if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
395
				$differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
396
				$differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
397
				continue;
398
			}
399
400
			// Should never happen.
401
			throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.');
402
		}
403
404
		return $differenceArray;
405
	}
406
407
	/**
408
	 * Whether the code integrity check has passed successful or not
409
	 *
410
	 * @return bool
411
	 */
412
	public function hasPassedCheck(): bool {
413
		$results = $this->getResults();
414
		if (empty($results)) {
415
			return true;
416
		}
417
418
		return false;
419
	}
420
421
	/**
422
	 * @return array
423
	 */
424
	public function getResults(): array {
425
		$cachedResults = $this->cache->get(self::CACHE_KEY);
426
		if (!\is_null($cachedResults)) {
427
			return json_decode($cachedResults, true);
428
		}
429
430
		if ($this->config !== null) {
431
			return json_decode($this->config->getAppValue('core', self::CACHE_KEY, '{}'), true);
432
		}
433
		return [];
434
	}
435
436
	/**
437
	 * Stores the results in the app config as well as cache
438
	 *
439
	 * @param string $scope
440
	 * @param array $result
441
	 */
442
	private function storeResults(string $scope, array $result) {
443
		$resultArray = $this->getResults();
444
		unset($resultArray[$scope]);
445
		if (!empty($result)) {
446
			$resultArray[$scope] = $result;
447
		}
448
		if ($this->config !== null) {
449
			$this->config->setAppValue('core', self::CACHE_KEY, json_encode($resultArray));
450
		}
451
		$this->cache->set(self::CACHE_KEY, json_encode($resultArray));
452
	}
453
454
	/**
455
	 *
456
	 * Clean previous results for a proper rescanning. Otherwise
457
	 */
458
	private function cleanResults() {
459
		$this->config->deleteAppValue('core', self::CACHE_KEY);
0 ignored issues
show
Bug introduced by
The method deleteAppValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

459
		$this->config->/** @scrutinizer ignore-call */ 
460
                 deleteAppValue('core', self::CACHE_KEY);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
460
		$this->cache->remove(self::CACHE_KEY);
461
	}
462
463
	/**
464
	 * Verify the signature of $appId. Returns an array with the following content:
465
	 * [
466
	 * 	'FILE_MISSING' =>
467
	 * 	[
468
	 * 		'filename' => [
469
	 * 			'expected' => 'expectedSHA512',
470
	 * 			'current' => 'currentSHA512',
471
	 * 		],
472
	 * 	],
473
	 * 	'EXTRA_FILE' =>
474
	 * 	[
475
	 * 		'filename' => [
476
	 * 			'expected' => 'expectedSHA512',
477
	 * 			'current' => 'currentSHA512',
478
	 * 		],
479
	 * 	],
480
	 * 	'INVALID_HASH' =>
481
	 * 	[
482
	 * 		'filename' => [
483
	 * 			'expected' => 'expectedSHA512',
484
	 * 			'current' => 'currentSHA512',
485
	 * 		],
486
	 * 	],
487
	 * ]
488
	 *
489
	 * Array may be empty in case no problems have been found.
490
	 *
491
	 * @param string $appId
492
	 * @param string $path Optional path. If none is given it will be guessed.
493
	 * @param bool $forceVerify
494
	 * @return array
495
	 */
496
	public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array {
497
		try {
498
			if ($path === '') {
499
				$path = $this->appLocator->getAppPath($appId);
500
			}
501
			$result = $this->verify(
502
					$path . '/appinfo/signature.json',
503
					$path,
504
					$appId,
505
					$forceVerify
506
			);
507
		} catch (\Exception $e) {
508
			$result = [
509
				'EXCEPTION' => [
510
					'class' => \get_class($e),
511
					'message' => $e->getMessage(),
512
				],
513
			];
514
		}
515
		$this->storeResults($appId, $result);
516
517
		return $result;
518
	}
519
520
	/**
521
	 * Verify the signature of core. Returns an array with the following content:
522
	 * [
523
	 * 	'FILE_MISSING' =>
524
	 * 	[
525
	 * 		'filename' => [
526
	 * 			'expected' => 'expectedSHA512',
527
	 * 			'current' => 'currentSHA512',
528
	 * 		],
529
	 * 	],
530
	 * 	'EXTRA_FILE' =>
531
	 * 	[
532
	 * 		'filename' => [
533
	 * 			'expected' => 'expectedSHA512',
534
	 * 			'current' => 'currentSHA512',
535
	 * 		],
536
	 * 	],
537
	 * 	'INVALID_HASH' =>
538
	 * 	[
539
	 * 		'filename' => [
540
	 * 			'expected' => 'expectedSHA512',
541
	 * 			'current' => 'currentSHA512',
542
	 * 		],
543
	 * 	],
544
	 * ]
545
	 *
546
	 * Array may be empty in case no problems have been found.
547
	 *
548
	 * @return array
549
	 */
550
	public function verifyCoreSignature(): array {
551
		try {
552
			$result = $this->verify(
553
					$this->environmentHelper->getServerRoot() . '/core/signature.json',
554
					$this->environmentHelper->getServerRoot(),
555
					'core'
556
			);
557
		} catch (\Exception $e) {
558
			$result = [
559
				'EXCEPTION' => [
560
					'class' => \get_class($e),
561
					'message' => $e->getMessage(),
562
				],
563
			];
564
		}
565
		$this->storeResults('core', $result);
566
567
		return $result;
568
	}
569
570
	/**
571
	 * Verify the core code of the instance as well as all applicable applications
572
	 * and store the results.
573
	 */
574
	public function runInstanceVerification() {
575
		$this->cleanResults();
576
		$this->verifyCoreSignature();
577
		$appIds = $this->appLocator->getAllApps();
578
		foreach ($appIds as $appId) {
579
			// If an application is shipped a valid signature is required
580
			$isShipped = $this->appManager->isShipped($appId);
0 ignored issues
show
Bug introduced by
The method isShipped() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

580
			/** @scrutinizer ignore-call */ 
581
   $isShipped = $this->appManager->isShipped($appId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
581
			$appNeedsToBeChecked = false;
582
			if ($isShipped) {
583
				$appNeedsToBeChecked = true;
584
			} elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) {
585
				// Otherwise only if the application explicitly ships a signature.json file
586
				$appNeedsToBeChecked = true;
587
			}
588
589
			if ($appNeedsToBeChecked) {
590
				$this->verifyAppSignature($appId);
591
			}
592
		}
593
	}
594
}
595