Completed
Pull Request — master (#3983)
by Joas
12:07
created
lib/private/IntegrityCheck/Checker.php 2 patches
Indentation   +551 added lines, -551 removed lines patch added patch discarded remove patch
@@ -49,555 +49,555 @@
 block discarded – undo
49 49
  * @package OC\IntegrityCheck
50 50
  */
51 51
 class Checker {
52
-	const CACHE_KEY = 'oc.integritycheck.checker';
53
-	/** @var EnvironmentHelper */
54
-	private $environmentHelper;
55
-	/** @var AppLocator */
56
-	private $appLocator;
57
-	/** @var FileAccessHelper */
58
-	private $fileAccessHelper;
59
-	/** @var IConfig */
60
-	private $config;
61
-	/** @var ICache */
62
-	private $cache;
63
-	/** @var IAppManager */
64
-	private $appManager;
65
-	/** @var ITempManager */
66
-	private $tempManager;
67
-
68
-	/**
69
-	 * @param EnvironmentHelper $environmentHelper
70
-	 * @param FileAccessHelper $fileAccessHelper
71
-	 * @param AppLocator $appLocator
72
-	 * @param IConfig $config
73
-	 * @param ICacheFactory $cacheFactory
74
-	 * @param IAppManager $appManager
75
-	 * @param ITempManager $tempManager
76
-	 */
77
-	public function __construct(EnvironmentHelper $environmentHelper,
78
-								FileAccessHelper $fileAccessHelper,
79
-								AppLocator $appLocator,
80
-								IConfig $config = null,
81
-								ICacheFactory $cacheFactory,
82
-								IAppManager $appManager = null,
83
-								ITempManager $tempManager) {
84
-		$this->environmentHelper = $environmentHelper;
85
-		$this->fileAccessHelper = $fileAccessHelper;
86
-		$this->appLocator = $appLocator;
87
-		$this->config = $config;
88
-		$this->cache = $cacheFactory->create(self::CACHE_KEY);
89
-		$this->appManager = $appManager;
90
-		$this->tempManager = $tempManager;
91
-	}
92
-
93
-	/**
94
-	 * Whether code signing is enforced or not.
95
-	 *
96
-	 * @return bool
97
-	 */
98
-	public function isCodeCheckEnforced() {
99
-		$notSignedChannels = [ '', 'git'];
100
-		if (in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) {
101
-			return false;
102
-		}
103
-
104
-		/**
105
-		 * This config option is undocumented and supposed to be so, it's only
106
-		 * applicable for very specific scenarios and we should not advertise it
107
-		 * too prominent. So please do not add it to config.sample.php.
108
-		 */
109
-		if ($this->config !== null) {
110
-			$isIntegrityCheckDisabled = $this->config->getSystemValue('integrity.check.disabled', false);
111
-		} else {
112
-			$isIntegrityCheckDisabled = false;
113
-		}
114
-		if ($isIntegrityCheckDisabled === true) {
115
-			return false;
116
-		}
117
-
118
-		return true;
119
-	}
120
-
121
-	/**
122
-	 * Enumerates all files belonging to the folder. Sensible defaults are excluded.
123
-	 *
124
-	 * @param string $folderToIterate
125
-	 * @param string $root
126
-	 * @return \RecursiveIteratorIterator
127
-	 * @throws \Exception
128
-	 */
129
-	private function getFolderIterator($folderToIterate, $root = '') {
130
-		$dirItr = new \RecursiveDirectoryIterator(
131
-			$folderToIterate,
132
-			\RecursiveDirectoryIterator::SKIP_DOTS
133
-		);
134
-		if($root === '') {
135
-			$root = \OC::$SERVERROOT;
136
-		}
137
-		$root = rtrim($root, '/');
138
-
139
-		$excludeGenericFilesIterator = new ExcludeFileByNameFilterIterator($dirItr);
140
-		$excludeFoldersIterator = new ExcludeFoldersByPathFilterIterator($excludeGenericFilesIterator, $root);
141
-
142
-		return new \RecursiveIteratorIterator(
143
-			$excludeFoldersIterator,
144
-			\RecursiveIteratorIterator::SELF_FIRST
145
-		);
146
-	}
147
-
148
-	/**
149
-	 * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found
150
-	 * in the iterator.
151
-	 *
152
-	 * @param \RecursiveIteratorIterator $iterator
153
-	 * @param string $path
154
-	 * @return array Array of hashes.
155
-	 */
156
-	private function generateHashes(\RecursiveIteratorIterator $iterator,
157
-									$path) {
158
-		$hashes = [];
159
-		$copiedWebserverSettingFiles = false;
160
-		$tmpFolder = '';
161
-
162
-		$baseDirectoryLength = strlen($path);
163
-		foreach($iterator as $filename => $data) {
164
-			/** @var \DirectoryIterator $data */
165
-			if($data->isDir()) {
166
-				continue;
167
-			}
168
-
169
-			$relativeFileName = substr($filename, $baseDirectoryLength);
170
-			$relativeFileName = ltrim($relativeFileName, '/');
171
-
172
-			// Exclude signature.json files in the appinfo and root folder
173
-			if($relativeFileName === 'appinfo/signature.json') {
174
-				continue;
175
-			}
176
-			// Exclude signature.json files in the appinfo and core folder
177
-			if($relativeFileName === 'core/signature.json') {
178
-				continue;
179
-			}
180
-
181
-			// The .user.ini and the .htaccess file of ownCloud can contain some
182
-			// custom modifications such as for example the maximum upload size
183
-			// to ensure that this will not lead to false positives this will
184
-			// copy the file to a temporary folder and reset it to the default
185
-			// values.
186
-			if($filename === $this->environmentHelper->getServerRoot() . '/.htaccess'
187
-				|| $filename === $this->environmentHelper->getServerRoot() . '/.user.ini') {
188
-
189
-				if(!$copiedWebserverSettingFiles) {
190
-					$tmpFolder = rtrim($this->tempManager->getTemporaryFolder(), '/');
191
-					copy($this->environmentHelper->getServerRoot() . '/.htaccess', $tmpFolder . '/.htaccess');
192
-					copy($this->environmentHelper->getServerRoot() . '/.user.ini', $tmpFolder . '/.user.ini');
193
-					\OC_Files::setUploadLimit(
194
-						\OCP\Util::computerFileSize('511MB'),
195
-						[
196
-							'.htaccess' => $tmpFolder . '/.htaccess',
197
-							'.user.ini' => $tmpFolder . '/.user.ini',
198
-						]
199
-					);
200
-				}
201
-			}
202
-
203
-			// The .user.ini file can contain custom modifications to the file size
204
-			// as well.
205
-			if($filename === $this->environmentHelper->getServerRoot() . '/.user.ini') {
206
-				$fileContent = file_get_contents($tmpFolder . '/.user.ini');
207
-				$hashes[$relativeFileName] = hash('sha512', $fileContent);
208
-				continue;
209
-			}
210
-
211
-			// The .htaccess file in the root folder of ownCloud can contain
212
-			// custom content after the installation due to the fact that dynamic
213
-			// content is written into it at installation time as well. This
214
-			// includes for example the 404 and 403 instructions.
215
-			// Thus we ignore everything below the first occurrence of
216
-			// "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the
217
-			// hash generated based on this.
218
-			if($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') {
219
-				$fileContent = file_get_contents($tmpFolder . '/.htaccess');
220
-				$explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent);
221
-				if(count($explodedArray) === 2) {
222
-					$hashes[$relativeFileName] = hash('sha512', $explodedArray[0]);
223
-					continue;
224
-				}
225
-			}
226
-
227
-			$hashes[$relativeFileName] = hash_file('sha512', $filename);
228
-		}
229
-
230
-		return $hashes;
231
-	}
232
-
233
-	/**
234
-	 * Creates the signature data
235
-	 *
236
-	 * @param array $hashes
237
-	 * @param X509 $certificate
238
-	 * @param RSA $privateKey
239
-	 * @return string
240
-	 */
241
-	private function createSignatureData(array $hashes,
242
-										 X509 $certificate,
243
-										 RSA $privateKey) {
244
-		ksort($hashes);
245
-
246
-		$privateKey->setSignatureMode(RSA::SIGNATURE_PSS);
247
-		$privateKey->setMGFHash('sha512');
248
-		// See https://tools.ietf.org/html/rfc3447#page-38
249
-		$privateKey->setSaltLength(0);
250
-		$signature = $privateKey->sign(json_encode($hashes));
251
-
252
-		return [
253
-				'hashes' => $hashes,
254
-				'signature' => base64_encode($signature),
255
-				'certificate' => $certificate->saveX509($certificate->currentCert),
256
-			];
257
-	}
258
-
259
-	/**
260
-	 * Write the signature of the app in the specified folder
261
-	 *
262
-	 * @param string $path
263
-	 * @param X509 $certificate
264
-	 * @param RSA $privateKey
265
-	 * @throws \Exception
266
-	 */
267
-	public function writeAppSignature($path,
268
-									  X509 $certificate,
269
-									  RSA $privateKey) {
270
-		$appInfoDir = $path . '/appinfo';
271
-		try {
272
-			$this->fileAccessHelper->assertDirectoryExists($appInfoDir);
273
-
274
-			$iterator = $this->getFolderIterator($path);
275
-			$hashes = $this->generateHashes($iterator, $path);
276
-			$signature = $this->createSignatureData($hashes, $certificate, $privateKey);
277
-				$this->fileAccessHelper->file_put_contents(
278
-					$appInfoDir . '/signature.json',
279
-				json_encode($signature, JSON_PRETTY_PRINT)
280
-			);
281
-		} catch (\Exception $e){
282
-			if (!$this->fileAccessHelper->is_writable($appInfoDir)) {
283
-				throw new \Exception($appInfoDir . ' is not writable');
284
-			}
285
-			throw $e;
286
-		}
287
-	}
288
-
289
-	/**
290
-	 * Write the signature of core
291
-	 *
292
-	 * @param X509 $certificate
293
-	 * @param RSA $rsa
294
-	 * @param string $path
295
-	 * @throws \Exception
296
-	 */
297
-	public function writeCoreSignature(X509 $certificate,
298
-									   RSA $rsa,
299
-									   $path) {
300
-		$coreDir = $path . '/core';
301
-		try {
302
-
303
-			$this->fileAccessHelper->assertDirectoryExists($coreDir);
304
-			$iterator = $this->getFolderIterator($path, $path);
305
-			$hashes = $this->generateHashes($iterator, $path);
306
-			$signatureData = $this->createSignatureData($hashes, $certificate, $rsa);
307
-			$this->fileAccessHelper->file_put_contents(
308
-				$coreDir . '/signature.json',
309
-				json_encode($signatureData, JSON_PRETTY_PRINT)
310
-			);
311
-		} catch (\Exception $e){
312
-			if (!$this->fileAccessHelper->is_writable($coreDir)) {
313
-				throw new \Exception($coreDir . ' is not writable');
314
-			}
315
-			throw $e;
316
-		}
317
-	}
318
-
319
-	/**
320
-	 * Verifies the signature for the specified path.
321
-	 *
322
-	 * @param string $signaturePath
323
-	 * @param string $basePath
324
-	 * @param string $certificateCN
325
-	 * @return array
326
-	 * @throws InvalidSignatureException
327
-	 * @throws \Exception
328
-	 */
329
-	private function verify($signaturePath, $basePath, $certificateCN) {
330
-		if(!$this->isCodeCheckEnforced()) {
331
-			return [];
332
-		}
333
-
334
-		$signatureData = json_decode($this->fileAccessHelper->file_get_contents($signaturePath), true);
335
-		if(!is_array($signatureData)) {
336
-			throw new InvalidSignatureException('Signature data not found.');
337
-		}
338
-
339
-		$expectedHashes = $signatureData['hashes'];
340
-		ksort($expectedHashes);
341
-		$signature = base64_decode($signatureData['signature']);
342
-		$certificate = $signatureData['certificate'];
343
-
344
-		// Check if certificate is signed by Nextcloud Root Authority
345
-		$x509 = new \phpseclib\File\X509();
346
-		$rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
347
-		$x509->loadCA($rootCertificatePublicKey);
348
-		$x509->loadX509($certificate);
349
-		if(!$x509->validateSignature()) {
350
-			throw new InvalidSignatureException('Certificate is not valid.');
351
-		}
352
-		// Verify if certificate has proper CN. "core" CN is always trusted.
353
-		if($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
354
-			throw new InvalidSignatureException(
355
-					sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
356
-			);
357
-		}
358
-
359
-		// Check if the signature of the files is valid
360
-		$rsa = new \phpseclib\Crypt\RSA();
361
-		$rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
362
-		$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
363
-		$rsa->setMGFHash('sha512');
364
-		// See https://tools.ietf.org/html/rfc3447#page-38
365
-		$rsa->setSaltLength(0);
366
-		if(!$rsa->verify(json_encode($expectedHashes), $signature)) {
367
-			throw new InvalidSignatureException('Signature could not get verified.');
368
-		}
369
-
370
-		// Fixes for the updater as shipped with ownCloud 9.0.x: The updater is
371
-		// replaced after the code integrity check is performed.
372
-		//
373
-		// Due to this reason we exclude the whole updater/ folder from the code
374
-		// integrity check.
375
-		if($basePath === $this->environmentHelper->getServerRoot()) {
376
-			foreach($expectedHashes as $fileName => $hash) {
377
-				if(strpos($fileName, 'updater/') === 0) {
378
-					unset($expectedHashes[$fileName]);
379
-				}
380
-			}
381
-		}
382
-
383
-		// Compare the list of files which are not identical
384
-		$currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath);
385
-		$differencesA = array_diff($expectedHashes, $currentInstanceHashes);
386
-		$differencesB = array_diff($currentInstanceHashes, $expectedHashes);
387
-		$differences = array_unique(array_merge($differencesA, $differencesB));
388
-		$differenceArray = [];
389
-		foreach($differences as $filename => $hash) {
390
-			// Check if file should not exist in the new signature table
391
-			if(!array_key_exists($filename, $expectedHashes)) {
392
-				$differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
393
-				$differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
394
-				continue;
395
-			}
396
-
397
-			// Check if file is missing
398
-			if(!array_key_exists($filename, $currentInstanceHashes)) {
399
-				$differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
400
-				$differenceArray['FILE_MISSING'][$filename]['current'] = '';
401
-				continue;
402
-			}
403
-
404
-			// Check if hash does mismatch
405
-			if($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
406
-				$differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
407
-				$differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
408
-				continue;
409
-			}
410
-
411
-			// Should never happen.
412
-			throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.');
413
-		}
414
-
415
-		return $differenceArray;
416
-	}
417
-
418
-	/**
419
-	 * Whether the code integrity check has passed successful or not
420
-	 *
421
-	 * @return bool
422
-	 */
423
-	public function hasPassedCheck() {
424
-		$results = $this->getResults();
425
-		if(empty($results)) {
426
-			return true;
427
-		}
428
-
429
-		return false;
430
-	}
431
-
432
-	/**
433
-	 * @return array
434
-	 */
435
-	public function getResults() {
436
-		$cachedResults = $this->cache->get(self::CACHE_KEY);
437
-		if(!is_null($cachedResults)) {
438
-			return json_decode($cachedResults, true);
439
-		}
440
-
441
-		if ($this->config !== null) {
442
-			return json_decode($this->config->getAppValue('core', self::CACHE_KEY, '{}'), true);
443
-		}
444
-		return [];
445
-	}
446
-
447
-	/**
448
-	 * Stores the results in the app config as well as cache
449
-	 *
450
-	 * @param string $scope
451
-	 * @param array $result
452
-	 */
453
-	private function storeResults($scope, array $result) {
454
-		$resultArray = $this->getResults();
455
-		unset($resultArray[$scope]);
456
-		if(!empty($result)) {
457
-			$resultArray[$scope] = $result;
458
-		}
459
-		if ($this->config !== null) {
460
-			$this->config->setAppValue('core', self::CACHE_KEY, json_encode($resultArray));
461
-		}
462
-		$this->cache->set(self::CACHE_KEY, json_encode($resultArray));
463
-	}
464
-
465
-	/**
466
-	 *
467
-	 * Clean previous results for a proper rescanning. Otherwise
468
-	 */
469
-	private function cleanResults() {
470
-		$this->config->deleteAppValue('core', self::CACHE_KEY);
471
-		$this->cache->remove(self::CACHE_KEY);
472
-	}
473
-
474
-	/**
475
-	 * Verify the signature of $appId. Returns an array with the following content:
476
-	 * [
477
-	 * 	'FILE_MISSING' =>
478
-	 * 	[
479
-	 * 		'filename' => [
480
-	 * 			'expected' => 'expectedSHA512',
481
-	 * 			'current' => 'currentSHA512',
482
-	 * 		],
483
-	 * 	],
484
-	 * 	'EXTRA_FILE' =>
485
-	 * 	[
486
-	 * 		'filename' => [
487
-	 * 			'expected' => 'expectedSHA512',
488
-	 * 			'current' => 'currentSHA512',
489
-	 * 		],
490
-	 * 	],
491
-	 * 	'INVALID_HASH' =>
492
-	 * 	[
493
-	 * 		'filename' => [
494
-	 * 			'expected' => 'expectedSHA512',
495
-	 * 			'current' => 'currentSHA512',
496
-	 * 		],
497
-	 * 	],
498
-	 * ]
499
-	 *
500
-	 * Array may be empty in case no problems have been found.
501
-	 *
502
-	 * @param string $appId
503
-	 * @param string $path Optional path. If none is given it will be guessed.
504
-	 * @return array
505
-	 */
506
-	public function verifyAppSignature($appId, $path = '') {
507
-		try {
508
-			if($path === '') {
509
-				$path = $this->appLocator->getAppPath($appId);
510
-			}
511
-			$result = $this->verify(
512
-					$path . '/appinfo/signature.json',
513
-					$path,
514
-					$appId
515
-			);
516
-		} catch (\Exception $e) {
517
-			$result = [
518
-					'EXCEPTION' => [
519
-							'class' => get_class($e),
520
-							'message' => $e->getMessage(),
521
-					],
522
-			];
523
-		}
524
-		$this->storeResults($appId, $result);
525
-
526
-		return $result;
527
-	}
528
-
529
-	/**
530
-	 * Verify the signature of core. Returns an array with the following content:
531
-	 * [
532
-	 * 	'FILE_MISSING' =>
533
-	 * 	[
534
-	 * 		'filename' => [
535
-	 * 			'expected' => 'expectedSHA512',
536
-	 * 			'current' => 'currentSHA512',
537
-	 * 		],
538
-	 * 	],
539
-	 * 	'EXTRA_FILE' =>
540
-	 * 	[
541
-	 * 		'filename' => [
542
-	 * 			'expected' => 'expectedSHA512',
543
-	 * 			'current' => 'currentSHA512',
544
-	 * 		],
545
-	 * 	],
546
-	 * 	'INVALID_HASH' =>
547
-	 * 	[
548
-	 * 		'filename' => [
549
-	 * 			'expected' => 'expectedSHA512',
550
-	 * 			'current' => 'currentSHA512',
551
-	 * 		],
552
-	 * 	],
553
-	 * ]
554
-	 *
555
-	 * Array may be empty in case no problems have been found.
556
-	 *
557
-	 * @return array
558
-	 */
559
-	public function verifyCoreSignature() {
560
-		try {
561
-			$result = $this->verify(
562
-					$this->environmentHelper->getServerRoot() . '/core/signature.json',
563
-					$this->environmentHelper->getServerRoot(),
564
-					'core'
565
-			);
566
-		} catch (\Exception $e) {
567
-			$result = [
568
-					'EXCEPTION' => [
569
-							'class' => get_class($e),
570
-							'message' => $e->getMessage(),
571
-					],
572
-			];
573
-		}
574
-		$this->storeResults('core', $result);
575
-
576
-		return $result;
577
-	}
578
-
579
-	/**
580
-	 * Verify the core code of the instance as well as all applicable applications
581
-	 * and store the results.
582
-	 */
583
-	public function runInstanceVerification() {
584
-		$this->cleanResults();
585
-		$this->verifyCoreSignature();
586
-		$appIds = $this->appLocator->getAllApps();
587
-		foreach($appIds as $appId) {
588
-			// If an application is shipped a valid signature is required
589
-			$isShipped = $this->appManager->isShipped($appId);
590
-			$appNeedsToBeChecked = false;
591
-			if ($isShipped) {
592
-				$appNeedsToBeChecked = true;
593
-			} elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) {
594
-				// Otherwise only if the application explicitly ships a signature.json file
595
-				$appNeedsToBeChecked = true;
596
-			}
597
-
598
-			if($appNeedsToBeChecked) {
599
-				$this->verifyAppSignature($appId);
600
-			}
601
-		}
602
-	}
52
+    const CACHE_KEY = 'oc.integritycheck.checker';
53
+    /** @var EnvironmentHelper */
54
+    private $environmentHelper;
55
+    /** @var AppLocator */
56
+    private $appLocator;
57
+    /** @var FileAccessHelper */
58
+    private $fileAccessHelper;
59
+    /** @var IConfig */
60
+    private $config;
61
+    /** @var ICache */
62
+    private $cache;
63
+    /** @var IAppManager */
64
+    private $appManager;
65
+    /** @var ITempManager */
66
+    private $tempManager;
67
+
68
+    /**
69
+     * @param EnvironmentHelper $environmentHelper
70
+     * @param FileAccessHelper $fileAccessHelper
71
+     * @param AppLocator $appLocator
72
+     * @param IConfig $config
73
+     * @param ICacheFactory $cacheFactory
74
+     * @param IAppManager $appManager
75
+     * @param ITempManager $tempManager
76
+     */
77
+    public function __construct(EnvironmentHelper $environmentHelper,
78
+                                FileAccessHelper $fileAccessHelper,
79
+                                AppLocator $appLocator,
80
+                                IConfig $config = null,
81
+                                ICacheFactory $cacheFactory,
82
+                                IAppManager $appManager = null,
83
+                                ITempManager $tempManager) {
84
+        $this->environmentHelper = $environmentHelper;
85
+        $this->fileAccessHelper = $fileAccessHelper;
86
+        $this->appLocator = $appLocator;
87
+        $this->config = $config;
88
+        $this->cache = $cacheFactory->create(self::CACHE_KEY);
89
+        $this->appManager = $appManager;
90
+        $this->tempManager = $tempManager;
91
+    }
92
+
93
+    /**
94
+     * Whether code signing is enforced or not.
95
+     *
96
+     * @return bool
97
+     */
98
+    public function isCodeCheckEnforced() {
99
+        $notSignedChannels = [ '', 'git'];
100
+        if (in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) {
101
+            return false;
102
+        }
103
+
104
+        /**
105
+         * This config option is undocumented and supposed to be so, it's only
106
+         * applicable for very specific scenarios and we should not advertise it
107
+         * too prominent. So please do not add it to config.sample.php.
108
+         */
109
+        if ($this->config !== null) {
110
+            $isIntegrityCheckDisabled = $this->config->getSystemValue('integrity.check.disabled', false);
111
+        } else {
112
+            $isIntegrityCheckDisabled = false;
113
+        }
114
+        if ($isIntegrityCheckDisabled === true) {
115
+            return false;
116
+        }
117
+
118
+        return true;
119
+    }
120
+
121
+    /**
122
+     * Enumerates all files belonging to the folder. Sensible defaults are excluded.
123
+     *
124
+     * @param string $folderToIterate
125
+     * @param string $root
126
+     * @return \RecursiveIteratorIterator
127
+     * @throws \Exception
128
+     */
129
+    private function getFolderIterator($folderToIterate, $root = '') {
130
+        $dirItr = new \RecursiveDirectoryIterator(
131
+            $folderToIterate,
132
+            \RecursiveDirectoryIterator::SKIP_DOTS
133
+        );
134
+        if($root === '') {
135
+            $root = \OC::$SERVERROOT;
136
+        }
137
+        $root = rtrim($root, '/');
138
+
139
+        $excludeGenericFilesIterator = new ExcludeFileByNameFilterIterator($dirItr);
140
+        $excludeFoldersIterator = new ExcludeFoldersByPathFilterIterator($excludeGenericFilesIterator, $root);
141
+
142
+        return new \RecursiveIteratorIterator(
143
+            $excludeFoldersIterator,
144
+            \RecursiveIteratorIterator::SELF_FIRST
145
+        );
146
+    }
147
+
148
+    /**
149
+     * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found
150
+     * in the iterator.
151
+     *
152
+     * @param \RecursiveIteratorIterator $iterator
153
+     * @param string $path
154
+     * @return array Array of hashes.
155
+     */
156
+    private function generateHashes(\RecursiveIteratorIterator $iterator,
157
+                                    $path) {
158
+        $hashes = [];
159
+        $copiedWebserverSettingFiles = false;
160
+        $tmpFolder = '';
161
+
162
+        $baseDirectoryLength = strlen($path);
163
+        foreach($iterator as $filename => $data) {
164
+            /** @var \DirectoryIterator $data */
165
+            if($data->isDir()) {
166
+                continue;
167
+            }
168
+
169
+            $relativeFileName = substr($filename, $baseDirectoryLength);
170
+            $relativeFileName = ltrim($relativeFileName, '/');
171
+
172
+            // Exclude signature.json files in the appinfo and root folder
173
+            if($relativeFileName === 'appinfo/signature.json') {
174
+                continue;
175
+            }
176
+            // Exclude signature.json files in the appinfo and core folder
177
+            if($relativeFileName === 'core/signature.json') {
178
+                continue;
179
+            }
180
+
181
+            // The .user.ini and the .htaccess file of ownCloud can contain some
182
+            // custom modifications such as for example the maximum upload size
183
+            // to ensure that this will not lead to false positives this will
184
+            // copy the file to a temporary folder and reset it to the default
185
+            // values.
186
+            if($filename === $this->environmentHelper->getServerRoot() . '/.htaccess'
187
+                || $filename === $this->environmentHelper->getServerRoot() . '/.user.ini') {
188
+
189
+                if(!$copiedWebserverSettingFiles) {
190
+                    $tmpFolder = rtrim($this->tempManager->getTemporaryFolder(), '/');
191
+                    copy($this->environmentHelper->getServerRoot() . '/.htaccess', $tmpFolder . '/.htaccess');
192
+                    copy($this->environmentHelper->getServerRoot() . '/.user.ini', $tmpFolder . '/.user.ini');
193
+                    \OC_Files::setUploadLimit(
194
+                        \OCP\Util::computerFileSize('511MB'),
195
+                        [
196
+                            '.htaccess' => $tmpFolder . '/.htaccess',
197
+                            '.user.ini' => $tmpFolder . '/.user.ini',
198
+                        ]
199
+                    );
200
+                }
201
+            }
202
+
203
+            // The .user.ini file can contain custom modifications to the file size
204
+            // as well.
205
+            if($filename === $this->environmentHelper->getServerRoot() . '/.user.ini') {
206
+                $fileContent = file_get_contents($tmpFolder . '/.user.ini');
207
+                $hashes[$relativeFileName] = hash('sha512', $fileContent);
208
+                continue;
209
+            }
210
+
211
+            // The .htaccess file in the root folder of ownCloud can contain
212
+            // custom content after the installation due to the fact that dynamic
213
+            // content is written into it at installation time as well. This
214
+            // includes for example the 404 and 403 instructions.
215
+            // Thus we ignore everything below the first occurrence of
216
+            // "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the
217
+            // hash generated based on this.
218
+            if($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') {
219
+                $fileContent = file_get_contents($tmpFolder . '/.htaccess');
220
+                $explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent);
221
+                if(count($explodedArray) === 2) {
222
+                    $hashes[$relativeFileName] = hash('sha512', $explodedArray[0]);
223
+                    continue;
224
+                }
225
+            }
226
+
227
+            $hashes[$relativeFileName] = hash_file('sha512', $filename);
228
+        }
229
+
230
+        return $hashes;
231
+    }
232
+
233
+    /**
234
+     * Creates the signature data
235
+     *
236
+     * @param array $hashes
237
+     * @param X509 $certificate
238
+     * @param RSA $privateKey
239
+     * @return string
240
+     */
241
+    private function createSignatureData(array $hashes,
242
+                                            X509 $certificate,
243
+                                            RSA $privateKey) {
244
+        ksort($hashes);
245
+
246
+        $privateKey->setSignatureMode(RSA::SIGNATURE_PSS);
247
+        $privateKey->setMGFHash('sha512');
248
+        // See https://tools.ietf.org/html/rfc3447#page-38
249
+        $privateKey->setSaltLength(0);
250
+        $signature = $privateKey->sign(json_encode($hashes));
251
+
252
+        return [
253
+                'hashes' => $hashes,
254
+                'signature' => base64_encode($signature),
255
+                'certificate' => $certificate->saveX509($certificate->currentCert),
256
+            ];
257
+    }
258
+
259
+    /**
260
+     * Write the signature of the app in the specified folder
261
+     *
262
+     * @param string $path
263
+     * @param X509 $certificate
264
+     * @param RSA $privateKey
265
+     * @throws \Exception
266
+     */
267
+    public function writeAppSignature($path,
268
+                                        X509 $certificate,
269
+                                        RSA $privateKey) {
270
+        $appInfoDir = $path . '/appinfo';
271
+        try {
272
+            $this->fileAccessHelper->assertDirectoryExists($appInfoDir);
273
+
274
+            $iterator = $this->getFolderIterator($path);
275
+            $hashes = $this->generateHashes($iterator, $path);
276
+            $signature = $this->createSignatureData($hashes, $certificate, $privateKey);
277
+                $this->fileAccessHelper->file_put_contents(
278
+                    $appInfoDir . '/signature.json',
279
+                json_encode($signature, JSON_PRETTY_PRINT)
280
+            );
281
+        } catch (\Exception $e){
282
+            if (!$this->fileAccessHelper->is_writable($appInfoDir)) {
283
+                throw new \Exception($appInfoDir . ' is not writable');
284
+            }
285
+            throw $e;
286
+        }
287
+    }
288
+
289
+    /**
290
+     * Write the signature of core
291
+     *
292
+     * @param X509 $certificate
293
+     * @param RSA $rsa
294
+     * @param string $path
295
+     * @throws \Exception
296
+     */
297
+    public function writeCoreSignature(X509 $certificate,
298
+                                        RSA $rsa,
299
+                                        $path) {
300
+        $coreDir = $path . '/core';
301
+        try {
302
+
303
+            $this->fileAccessHelper->assertDirectoryExists($coreDir);
304
+            $iterator = $this->getFolderIterator($path, $path);
305
+            $hashes = $this->generateHashes($iterator, $path);
306
+            $signatureData = $this->createSignatureData($hashes, $certificate, $rsa);
307
+            $this->fileAccessHelper->file_put_contents(
308
+                $coreDir . '/signature.json',
309
+                json_encode($signatureData, JSON_PRETTY_PRINT)
310
+            );
311
+        } catch (\Exception $e){
312
+            if (!$this->fileAccessHelper->is_writable($coreDir)) {
313
+                throw new \Exception($coreDir . ' is not writable');
314
+            }
315
+            throw $e;
316
+        }
317
+    }
318
+
319
+    /**
320
+     * Verifies the signature for the specified path.
321
+     *
322
+     * @param string $signaturePath
323
+     * @param string $basePath
324
+     * @param string $certificateCN
325
+     * @return array
326
+     * @throws InvalidSignatureException
327
+     * @throws \Exception
328
+     */
329
+    private function verify($signaturePath, $basePath, $certificateCN) {
330
+        if(!$this->isCodeCheckEnforced()) {
331
+            return [];
332
+        }
333
+
334
+        $signatureData = json_decode($this->fileAccessHelper->file_get_contents($signaturePath), true);
335
+        if(!is_array($signatureData)) {
336
+            throw new InvalidSignatureException('Signature data not found.');
337
+        }
338
+
339
+        $expectedHashes = $signatureData['hashes'];
340
+        ksort($expectedHashes);
341
+        $signature = base64_decode($signatureData['signature']);
342
+        $certificate = $signatureData['certificate'];
343
+
344
+        // Check if certificate is signed by Nextcloud Root Authority
345
+        $x509 = new \phpseclib\File\X509();
346
+        $rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
347
+        $x509->loadCA($rootCertificatePublicKey);
348
+        $x509->loadX509($certificate);
349
+        if(!$x509->validateSignature()) {
350
+            throw new InvalidSignatureException('Certificate is not valid.');
351
+        }
352
+        // Verify if certificate has proper CN. "core" CN is always trusted.
353
+        if($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
354
+            throw new InvalidSignatureException(
355
+                    sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
356
+            );
357
+        }
358
+
359
+        // Check if the signature of the files is valid
360
+        $rsa = new \phpseclib\Crypt\RSA();
361
+        $rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
362
+        $rsa->setSignatureMode(RSA::SIGNATURE_PSS);
363
+        $rsa->setMGFHash('sha512');
364
+        // See https://tools.ietf.org/html/rfc3447#page-38
365
+        $rsa->setSaltLength(0);
366
+        if(!$rsa->verify(json_encode($expectedHashes), $signature)) {
367
+            throw new InvalidSignatureException('Signature could not get verified.');
368
+        }
369
+
370
+        // Fixes for the updater as shipped with ownCloud 9.0.x: The updater is
371
+        // replaced after the code integrity check is performed.
372
+        //
373
+        // Due to this reason we exclude the whole updater/ folder from the code
374
+        // integrity check.
375
+        if($basePath === $this->environmentHelper->getServerRoot()) {
376
+            foreach($expectedHashes as $fileName => $hash) {
377
+                if(strpos($fileName, 'updater/') === 0) {
378
+                    unset($expectedHashes[$fileName]);
379
+                }
380
+            }
381
+        }
382
+
383
+        // Compare the list of files which are not identical
384
+        $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath);
385
+        $differencesA = array_diff($expectedHashes, $currentInstanceHashes);
386
+        $differencesB = array_diff($currentInstanceHashes, $expectedHashes);
387
+        $differences = array_unique(array_merge($differencesA, $differencesB));
388
+        $differenceArray = [];
389
+        foreach($differences as $filename => $hash) {
390
+            // Check if file should not exist in the new signature table
391
+            if(!array_key_exists($filename, $expectedHashes)) {
392
+                $differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
393
+                $differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
394
+                continue;
395
+            }
396
+
397
+            // Check if file is missing
398
+            if(!array_key_exists($filename, $currentInstanceHashes)) {
399
+                $differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
400
+                $differenceArray['FILE_MISSING'][$filename]['current'] = '';
401
+                continue;
402
+            }
403
+
404
+            // Check if hash does mismatch
405
+            if($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
406
+                $differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
407
+                $differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
408
+                continue;
409
+            }
410
+
411
+            // Should never happen.
412
+            throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.');
413
+        }
414
+
415
+        return $differenceArray;
416
+    }
417
+
418
+    /**
419
+     * Whether the code integrity check has passed successful or not
420
+     *
421
+     * @return bool
422
+     */
423
+    public function hasPassedCheck() {
424
+        $results = $this->getResults();
425
+        if(empty($results)) {
426
+            return true;
427
+        }
428
+
429
+        return false;
430
+    }
431
+
432
+    /**
433
+     * @return array
434
+     */
435
+    public function getResults() {
436
+        $cachedResults = $this->cache->get(self::CACHE_KEY);
437
+        if(!is_null($cachedResults)) {
438
+            return json_decode($cachedResults, true);
439
+        }
440
+
441
+        if ($this->config !== null) {
442
+            return json_decode($this->config->getAppValue('core', self::CACHE_KEY, '{}'), true);
443
+        }
444
+        return [];
445
+    }
446
+
447
+    /**
448
+     * Stores the results in the app config as well as cache
449
+     *
450
+     * @param string $scope
451
+     * @param array $result
452
+     */
453
+    private function storeResults($scope, array $result) {
454
+        $resultArray = $this->getResults();
455
+        unset($resultArray[$scope]);
456
+        if(!empty($result)) {
457
+            $resultArray[$scope] = $result;
458
+        }
459
+        if ($this->config !== null) {
460
+            $this->config->setAppValue('core', self::CACHE_KEY, json_encode($resultArray));
461
+        }
462
+        $this->cache->set(self::CACHE_KEY, json_encode($resultArray));
463
+    }
464
+
465
+    /**
466
+     *
467
+     * Clean previous results for a proper rescanning. Otherwise
468
+     */
469
+    private function cleanResults() {
470
+        $this->config->deleteAppValue('core', self::CACHE_KEY);
471
+        $this->cache->remove(self::CACHE_KEY);
472
+    }
473
+
474
+    /**
475
+     * Verify the signature of $appId. Returns an array with the following content:
476
+     * [
477
+     * 	'FILE_MISSING' =>
478
+     * 	[
479
+     * 		'filename' => [
480
+     * 			'expected' => 'expectedSHA512',
481
+     * 			'current' => 'currentSHA512',
482
+     * 		],
483
+     * 	],
484
+     * 	'EXTRA_FILE' =>
485
+     * 	[
486
+     * 		'filename' => [
487
+     * 			'expected' => 'expectedSHA512',
488
+     * 			'current' => 'currentSHA512',
489
+     * 		],
490
+     * 	],
491
+     * 	'INVALID_HASH' =>
492
+     * 	[
493
+     * 		'filename' => [
494
+     * 			'expected' => 'expectedSHA512',
495
+     * 			'current' => 'currentSHA512',
496
+     * 		],
497
+     * 	],
498
+     * ]
499
+     *
500
+     * Array may be empty in case no problems have been found.
501
+     *
502
+     * @param string $appId
503
+     * @param string $path Optional path. If none is given it will be guessed.
504
+     * @return array
505
+     */
506
+    public function verifyAppSignature($appId, $path = '') {
507
+        try {
508
+            if($path === '') {
509
+                $path = $this->appLocator->getAppPath($appId);
510
+            }
511
+            $result = $this->verify(
512
+                    $path . '/appinfo/signature.json',
513
+                    $path,
514
+                    $appId
515
+            );
516
+        } catch (\Exception $e) {
517
+            $result = [
518
+                    'EXCEPTION' => [
519
+                            'class' => get_class($e),
520
+                            'message' => $e->getMessage(),
521
+                    ],
522
+            ];
523
+        }
524
+        $this->storeResults($appId, $result);
525
+
526
+        return $result;
527
+    }
528
+
529
+    /**
530
+     * Verify the signature of core. Returns an array with the following content:
531
+     * [
532
+     * 	'FILE_MISSING' =>
533
+     * 	[
534
+     * 		'filename' => [
535
+     * 			'expected' => 'expectedSHA512',
536
+     * 			'current' => 'currentSHA512',
537
+     * 		],
538
+     * 	],
539
+     * 	'EXTRA_FILE' =>
540
+     * 	[
541
+     * 		'filename' => [
542
+     * 			'expected' => 'expectedSHA512',
543
+     * 			'current' => 'currentSHA512',
544
+     * 		],
545
+     * 	],
546
+     * 	'INVALID_HASH' =>
547
+     * 	[
548
+     * 		'filename' => [
549
+     * 			'expected' => 'expectedSHA512',
550
+     * 			'current' => 'currentSHA512',
551
+     * 		],
552
+     * 	],
553
+     * ]
554
+     *
555
+     * Array may be empty in case no problems have been found.
556
+     *
557
+     * @return array
558
+     */
559
+    public function verifyCoreSignature() {
560
+        try {
561
+            $result = $this->verify(
562
+                    $this->environmentHelper->getServerRoot() . '/core/signature.json',
563
+                    $this->environmentHelper->getServerRoot(),
564
+                    'core'
565
+            );
566
+        } catch (\Exception $e) {
567
+            $result = [
568
+                    'EXCEPTION' => [
569
+                            'class' => get_class($e),
570
+                            'message' => $e->getMessage(),
571
+                    ],
572
+            ];
573
+        }
574
+        $this->storeResults('core', $result);
575
+
576
+        return $result;
577
+    }
578
+
579
+    /**
580
+     * Verify the core code of the instance as well as all applicable applications
581
+     * and store the results.
582
+     */
583
+    public function runInstanceVerification() {
584
+        $this->cleanResults();
585
+        $this->verifyCoreSignature();
586
+        $appIds = $this->appLocator->getAllApps();
587
+        foreach($appIds as $appId) {
588
+            // If an application is shipped a valid signature is required
589
+            $isShipped = $this->appManager->isShipped($appId);
590
+            $appNeedsToBeChecked = false;
591
+            if ($isShipped) {
592
+                $appNeedsToBeChecked = true;
593
+            } elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) {
594
+                // Otherwise only if the application explicitly ships a signature.json file
595
+                $appNeedsToBeChecked = true;
596
+            }
597
+
598
+            if($appNeedsToBeChecked) {
599
+                $this->verifyAppSignature($appId);
600
+            }
601
+        }
602
+    }
603 603
 }
Please login to merge, or discard this patch.
Spacing   +47 added lines, -47 removed lines patch added patch discarded remove patch
@@ -96,7 +96,7 @@  discard block
 block discarded – undo
96 96
 	 * @return bool
97 97
 	 */
98 98
 	public function isCodeCheckEnforced() {
99
-		$notSignedChannels = [ '', 'git'];
99
+		$notSignedChannels = ['', 'git'];
100 100
 		if (in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) {
101 101
 			return false;
102 102
 		}
@@ -131,7 +131,7 @@  discard block
 block discarded – undo
131 131
 			$folderToIterate,
132 132
 			\RecursiveDirectoryIterator::SKIP_DOTS
133 133
 		);
134
-		if($root === '') {
134
+		if ($root === '') {
135 135
 			$root = \OC::$SERVERROOT;
136 136
 		}
137 137
 		$root = rtrim($root, '/');
@@ -160,9 +160,9 @@  discard block
 block discarded – undo
160 160
 		$tmpFolder = '';
161 161
 
162 162
 		$baseDirectoryLength = strlen($path);
163
-		foreach($iterator as $filename => $data) {
163
+		foreach ($iterator as $filename => $data) {
164 164
 			/** @var \DirectoryIterator $data */
165
-			if($data->isDir()) {
165
+			if ($data->isDir()) {
166 166
 				continue;
167 167
 			}
168 168
 
@@ -170,11 +170,11 @@  discard block
 block discarded – undo
170 170
 			$relativeFileName = ltrim($relativeFileName, '/');
171 171
 
172 172
 			// Exclude signature.json files in the appinfo and root folder
173
-			if($relativeFileName === 'appinfo/signature.json') {
173
+			if ($relativeFileName === 'appinfo/signature.json') {
174 174
 				continue;
175 175
 			}
176 176
 			// Exclude signature.json files in the appinfo and core folder
177
-			if($relativeFileName === 'core/signature.json') {
177
+			if ($relativeFileName === 'core/signature.json') {
178 178
 				continue;
179 179
 			}
180 180
 
@@ -183,18 +183,18 @@  discard block
 block discarded – undo
183 183
 			// to ensure that this will not lead to false positives this will
184 184
 			// copy the file to a temporary folder and reset it to the default
185 185
 			// values.
186
-			if($filename === $this->environmentHelper->getServerRoot() . '/.htaccess'
187
-				|| $filename === $this->environmentHelper->getServerRoot() . '/.user.ini') {
186
+			if ($filename === $this->environmentHelper->getServerRoot().'/.htaccess'
187
+				|| $filename === $this->environmentHelper->getServerRoot().'/.user.ini') {
188 188
 
189
-				if(!$copiedWebserverSettingFiles) {
189
+				if (!$copiedWebserverSettingFiles) {
190 190
 					$tmpFolder = rtrim($this->tempManager->getTemporaryFolder(), '/');
191
-					copy($this->environmentHelper->getServerRoot() . '/.htaccess', $tmpFolder . '/.htaccess');
192
-					copy($this->environmentHelper->getServerRoot() . '/.user.ini', $tmpFolder . '/.user.ini');
191
+					copy($this->environmentHelper->getServerRoot().'/.htaccess', $tmpFolder.'/.htaccess');
192
+					copy($this->environmentHelper->getServerRoot().'/.user.ini', $tmpFolder.'/.user.ini');
193 193
 					\OC_Files::setUploadLimit(
194 194
 						\OCP\Util::computerFileSize('511MB'),
195 195
 						[
196
-							'.htaccess' => $tmpFolder . '/.htaccess',
197
-							'.user.ini' => $tmpFolder . '/.user.ini',
196
+							'.htaccess' => $tmpFolder.'/.htaccess',
197
+							'.user.ini' => $tmpFolder.'/.user.ini',
198 198
 						]
199 199
 					);
200 200
 				}
@@ -202,8 +202,8 @@  discard block
 block discarded – undo
202 202
 
203 203
 			// The .user.ini file can contain custom modifications to the file size
204 204
 			// as well.
205
-			if($filename === $this->environmentHelper->getServerRoot() . '/.user.ini') {
206
-				$fileContent = file_get_contents($tmpFolder . '/.user.ini');
205
+			if ($filename === $this->environmentHelper->getServerRoot().'/.user.ini') {
206
+				$fileContent = file_get_contents($tmpFolder.'/.user.ini');
207 207
 				$hashes[$relativeFileName] = hash('sha512', $fileContent);
208 208
 				continue;
209 209
 			}
@@ -215,10 +215,10 @@  discard block
 block discarded – undo
215 215
 			// Thus we ignore everything below the first occurrence of
216 216
 			// "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the
217 217
 			// hash generated based on this.
218
-			if($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') {
219
-				$fileContent = file_get_contents($tmpFolder . '/.htaccess');
218
+			if ($filename === $this->environmentHelper->getServerRoot().'/.htaccess') {
219
+				$fileContent = file_get_contents($tmpFolder.'/.htaccess');
220 220
 				$explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent);
221
-				if(count($explodedArray) === 2) {
221
+				if (count($explodedArray) === 2) {
222 222
 					$hashes[$relativeFileName] = hash('sha512', $explodedArray[0]);
223 223
 					continue;
224 224
 				}
@@ -267,7 +267,7 @@  discard block
 block discarded – undo
267 267
 	public function writeAppSignature($path,
268 268
 									  X509 $certificate,
269 269
 									  RSA $privateKey) {
270
-		$appInfoDir = $path . '/appinfo';
270
+		$appInfoDir = $path.'/appinfo';
271 271
 		try {
272 272
 			$this->fileAccessHelper->assertDirectoryExists($appInfoDir);
273 273
 
@@ -275,12 +275,12 @@  discard block
 block discarded – undo
275 275
 			$hashes = $this->generateHashes($iterator, $path);
276 276
 			$signature = $this->createSignatureData($hashes, $certificate, $privateKey);
277 277
 				$this->fileAccessHelper->file_put_contents(
278
-					$appInfoDir . '/signature.json',
278
+					$appInfoDir.'/signature.json',
279 279
 				json_encode($signature, JSON_PRETTY_PRINT)
280 280
 			);
281
-		} catch (\Exception $e){
281
+		} catch (\Exception $e) {
282 282
 			if (!$this->fileAccessHelper->is_writable($appInfoDir)) {
283
-				throw new \Exception($appInfoDir . ' is not writable');
283
+				throw new \Exception($appInfoDir.' is not writable');
284 284
 			}
285 285
 			throw $e;
286 286
 		}
@@ -297,7 +297,7 @@  discard block
 block discarded – undo
297 297
 	public function writeCoreSignature(X509 $certificate,
298 298
 									   RSA $rsa,
299 299
 									   $path) {
300
-		$coreDir = $path . '/core';
300
+		$coreDir = $path.'/core';
301 301
 		try {
302 302
 
303 303
 			$this->fileAccessHelper->assertDirectoryExists($coreDir);
@@ -305,12 +305,12 @@  discard block
 block discarded – undo
305 305
 			$hashes = $this->generateHashes($iterator, $path);
306 306
 			$signatureData = $this->createSignatureData($hashes, $certificate, $rsa);
307 307
 			$this->fileAccessHelper->file_put_contents(
308
-				$coreDir . '/signature.json',
308
+				$coreDir.'/signature.json',
309 309
 				json_encode($signatureData, JSON_PRETTY_PRINT)
310 310
 			);
311
-		} catch (\Exception $e){
311
+		} catch (\Exception $e) {
312 312
 			if (!$this->fileAccessHelper->is_writable($coreDir)) {
313
-				throw new \Exception($coreDir . ' is not writable');
313
+				throw new \Exception($coreDir.' is not writable');
314 314
 			}
315 315
 			throw $e;
316 316
 		}
@@ -327,12 +327,12 @@  discard block
 block discarded – undo
327 327
 	 * @throws \Exception
328 328
 	 */
329 329
 	private function verify($signaturePath, $basePath, $certificateCN) {
330
-		if(!$this->isCodeCheckEnforced()) {
330
+		if (!$this->isCodeCheckEnforced()) {
331 331
 			return [];
332 332
 		}
333 333
 
334 334
 		$signatureData = json_decode($this->fileAccessHelper->file_get_contents($signaturePath), true);
335
-		if(!is_array($signatureData)) {
335
+		if (!is_array($signatureData)) {
336 336
 			throw new InvalidSignatureException('Signature data not found.');
337 337
 		}
338 338
 
@@ -346,11 +346,11 @@  discard block
 block discarded – undo
346 346
 		$rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
347 347
 		$x509->loadCA($rootCertificatePublicKey);
348 348
 		$x509->loadX509($certificate);
349
-		if(!$x509->validateSignature()) {
349
+		if (!$x509->validateSignature()) {
350 350
 			throw new InvalidSignatureException('Certificate is not valid.');
351 351
 		}
352 352
 		// Verify if certificate has proper CN. "core" CN is always trusted.
353
-		if($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
353
+		if ($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
354 354
 			throw new InvalidSignatureException(
355 355
 					sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
356 356
 			);
@@ -363,7 +363,7 @@  discard block
 block discarded – undo
363 363
 		$rsa->setMGFHash('sha512');
364 364
 		// See https://tools.ietf.org/html/rfc3447#page-38
365 365
 		$rsa->setSaltLength(0);
366
-		if(!$rsa->verify(json_encode($expectedHashes), $signature)) {
366
+		if (!$rsa->verify(json_encode($expectedHashes), $signature)) {
367 367
 			throw new InvalidSignatureException('Signature could not get verified.');
368 368
 		}
369 369
 
@@ -372,9 +372,9 @@  discard block
 block discarded – undo
372 372
 		//
373 373
 		// Due to this reason we exclude the whole updater/ folder from the code
374 374
 		// integrity check.
375
-		if($basePath === $this->environmentHelper->getServerRoot()) {
376
-			foreach($expectedHashes as $fileName => $hash) {
377
-				if(strpos($fileName, 'updater/') === 0) {
375
+		if ($basePath === $this->environmentHelper->getServerRoot()) {
376
+			foreach ($expectedHashes as $fileName => $hash) {
377
+				if (strpos($fileName, 'updater/') === 0) {
378 378
 					unset($expectedHashes[$fileName]);
379 379
 				}
380 380
 			}
@@ -386,23 +386,23 @@  discard block
 block discarded – undo
386 386
 		$differencesB = array_diff($currentInstanceHashes, $expectedHashes);
387 387
 		$differences = array_unique(array_merge($differencesA, $differencesB));
388 388
 		$differenceArray = [];
389
-		foreach($differences as $filename => $hash) {
389
+		foreach ($differences as $filename => $hash) {
390 390
 			// Check if file should not exist in the new signature table
391
-			if(!array_key_exists($filename, $expectedHashes)) {
391
+			if (!array_key_exists($filename, $expectedHashes)) {
392 392
 				$differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
393 393
 				$differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
394 394
 				continue;
395 395
 			}
396 396
 
397 397
 			// Check if file is missing
398
-			if(!array_key_exists($filename, $currentInstanceHashes)) {
398
+			if (!array_key_exists($filename, $currentInstanceHashes)) {
399 399
 				$differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
400 400
 				$differenceArray['FILE_MISSING'][$filename]['current'] = '';
401 401
 				continue;
402 402
 			}
403 403
 
404 404
 			// Check if hash does mismatch
405
-			if($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
405
+			if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
406 406
 				$differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
407 407
 				$differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
408 408
 				continue;
@@ -422,7 +422,7 @@  discard block
 block discarded – undo
422 422
 	 */
423 423
 	public function hasPassedCheck() {
424 424
 		$results = $this->getResults();
425
-		if(empty($results)) {
425
+		if (empty($results)) {
426 426
 			return true;
427 427
 		}
428 428
 
@@ -434,7 +434,7 @@  discard block
 block discarded – undo
434 434
 	 */
435 435
 	public function getResults() {
436 436
 		$cachedResults = $this->cache->get(self::CACHE_KEY);
437
-		if(!is_null($cachedResults)) {
437
+		if (!is_null($cachedResults)) {
438 438
 			return json_decode($cachedResults, true);
439 439
 		}
440 440
 
@@ -453,7 +453,7 @@  discard block
 block discarded – undo
453 453
 	private function storeResults($scope, array $result) {
454 454
 		$resultArray = $this->getResults();
455 455
 		unset($resultArray[$scope]);
456
-		if(!empty($result)) {
456
+		if (!empty($result)) {
457 457
 			$resultArray[$scope] = $result;
458 458
 		}
459 459
 		if ($this->config !== null) {
@@ -505,11 +505,11 @@  discard block
 block discarded – undo
505 505
 	 */
506 506
 	public function verifyAppSignature($appId, $path = '') {
507 507
 		try {
508
-			if($path === '') {
508
+			if ($path === '') {
509 509
 				$path = $this->appLocator->getAppPath($appId);
510 510
 			}
511 511
 			$result = $this->verify(
512
-					$path . '/appinfo/signature.json',
512
+					$path.'/appinfo/signature.json',
513 513
 					$path,
514 514
 					$appId
515 515
 			);
@@ -559,7 +559,7 @@  discard block
 block discarded – undo
559 559
 	public function verifyCoreSignature() {
560 560
 		try {
561 561
 			$result = $this->verify(
562
-					$this->environmentHelper->getServerRoot() . '/core/signature.json',
562
+					$this->environmentHelper->getServerRoot().'/core/signature.json',
563 563
 					$this->environmentHelper->getServerRoot(),
564 564
 					'core'
565 565
 			);
@@ -584,18 +584,18 @@  discard block
 block discarded – undo
584 584
 		$this->cleanResults();
585 585
 		$this->verifyCoreSignature();
586 586
 		$appIds = $this->appLocator->getAllApps();
587
-		foreach($appIds as $appId) {
587
+		foreach ($appIds as $appId) {
588 588
 			// If an application is shipped a valid signature is required
589 589
 			$isShipped = $this->appManager->isShipped($appId);
590 590
 			$appNeedsToBeChecked = false;
591 591
 			if ($isShipped) {
592 592
 				$appNeedsToBeChecked = true;
593
-			} elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) {
593
+			} elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId).'/appinfo/signature.json')) {
594 594
 				// Otherwise only if the application explicitly ships a signature.json file
595 595
 				$appNeedsToBeChecked = true;
596 596
 			}
597 597
 
598
-			if($appNeedsToBeChecked) {
598
+			if ($appNeedsToBeChecked) {
599 599
 				$this->verifyAppSignature($appId);
600 600
 			}
601 601
 		}
Please login to merge, or discard this patch.