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