Compare commits

...

2 Commits

Author SHA1 Message Date
Roeland Jago Douma ac27507f0f
Create V2 signatures if the minimum target NC is 19
The v2 signature is also reproducable in other languages. As the hashes
are just stored as a single string that is signed. This is then also
verified.

To be backwards compatible such v2 signatures should only be generated
for apps that have a minimum version of NC19

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2019-12-18 20:55:21 +01:00
Roeland Jago Douma 20ce9e89c1
Remove not needed temp manager
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2019-12-18 20:35:47 +01:00
3 changed files with 68 additions and 30 deletions

View File

@ -71,28 +71,15 @@ class Checker {
private $cache;
/** @var IAppManager */
private $appManager;
/** @var ITempManager */
private $tempManager;
/** @var IMimeTypeDetector */
private $mimeTypeDetector;
/**
* @param EnvironmentHelper $environmentHelper
* @param FileAccessHelper $fileAccessHelper
* @param AppLocator $appLocator
* @param IConfig $config
* @param ICacheFactory $cacheFactory
* @param IAppManager $appManager
* @param ITempManager $tempManager
* @param IMimeTypeDetector $mimeTypeDetector
*/
public function __construct(EnvironmentHelper $environmentHelper,
FileAccessHelper $fileAccessHelper,
AppLocator $appLocator,
IConfig $config = null,
ICacheFactory $cacheFactory,
IAppManager $appManager = null,
ITempManager $tempManager,
IMimeTypeDetector $mimeTypeDetector) {
$this->environmentHelper = $environmentHelper;
$this->fileAccessHelper = $fileAccessHelper;
@ -100,7 +87,6 @@ class Checker {
$this->config = $config;
$this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
$this->appManager = $appManager;
$this->tempManager = $tempManager;
$this->mimeTypeDetector = $mimeTypeDetector;
}
@ -229,19 +215,28 @@ class Checker {
*/
private function createSignatureData(array $hashes,
X509 $certificate,
RSA $privateKey): array {
RSA $privateKey,
int $version): array {
ksort($hashes);
$privateKey->setSignatureMode(RSA::SIGNATURE_PSS);
$privateKey->setMGFHash('sha512');
// See https://tools.ietf.org/html/rfc3447#page-38
$privateKey->setSaltLength(0);
$signature = $privateKey->sign(json_encode($hashes));
$encodedHashed = json_encode($hashes);
$signature = $privateKey->sign($encodedHashed);
$hashesOutput = $hashes;
if ($version === 2) {
$hashesOutput = $encodedHashed;
}
return [
'hashes' => $hashes,
'hashes' => $hashesOutput,
'signature' => base64_encode($signature),
'certificate' => $certificate->saveX509($certificate->currentCert),
'version' => 2,
];
}
@ -253,16 +248,18 @@ class Checker {
* @param RSA $privateKey
* @throws \Exception
*/
public function writeAppSignature($path,
public function writeAppSignature(string $path,
X509 $certificate,
RSA $privateKey) {
$appInfoDir = $path . '/appinfo';
try {
$this->fileAccessHelper->assertDirectoryExists($appInfoDir);
$version = $this->maxSignatureVersion($appInfoDir . '/info.xml');
$iterator = $this->getFolderIterator($path);
$hashes = $this->generateHashes($iterator, $path);
$signature = $this->createSignatureData($hashes, $certificate, $privateKey);
$signature = $this->createSignatureData($hashes, $certificate, $privateKey, $version);
$this->fileAccessHelper->file_put_contents(
$appInfoDir . '/signature.json',
json_encode($signature, JSON_PRETTY_PRINT)
@ -275,6 +272,34 @@ class Checker {
}
}
/**
* Checkl if we can sign the app with the v2 signature
*/
private function maxSignatureVersion(string $appInfoPath): int {
if (!file_exists($appInfoPath)) {
return 1;
}
$content = file_get_contents($appInfoPath);
try {
$xml = new \SimpleXMLElement($content);
$minNextcloudVersion = $xml->dependencies->nextcloud['min-version'];
if ($minNextcloudVersion === NULL) {
return 1;
}
if ((int)$minNextcloudVersion >= 19) {
return 2;
}
} catch (\Exception $e) {
return 1;
}
return 2;
}
/**
* Write the signature of core
*
@ -285,14 +310,14 @@ class Checker {
*/
public function writeCoreSignature(X509 $certificate,
RSA $rsa,
$path) {
string $path) {
$coreDir = $path . '/core';
try {
$this->fileAccessHelper->assertDirectoryExists($coreDir);
$iterator = $this->getFolderIterator($path, $path);
$hashes = $this->generateHashes($iterator, $path);
$signatureData = $this->createSignatureData($hashes, $certificate, $rsa);
$signatureData = $this->createSignatureData($hashes, $certificate, $rsa, 2);
$this->fileAccessHelper->file_put_contents(
$coreDir . '/signature.json',
json_encode($signatureData, JSON_PRETTY_PRINT)
@ -330,8 +355,6 @@ class Checker {
throw new InvalidSignatureException('Signature data not found.');
}
$expectedHashes = $signatureData['hashes'];
ksort($expectedHashes);
$signature = base64_decode($signatureData['signature']);
$certificate = $signatureData['certificate'];
@ -357,10 +380,27 @@ class Checker {
$rsa->setMGFHash('sha512');
// See https://tools.ietf.org/html/rfc3447#page-38
$rsa->setSaltLength(0);
if(!$rsa->verify(json_encode($expectedHashes), $signature)) {
$version = 1;
if (isset($signatureData['version'])) {
$version = $signatureData['version'];
}
if ($version === 1) {
$expectedHashes = $signatureData['hashes'];
ksort($expectedHashes);
$expectedHashes = json_encode($expectedHashes);
}
if ($version === 2) {
$expectedHashes = $signatureData['hashes'];
}
if(!$rsa->verify($expectedHashes, $signature)) {
throw new InvalidSignatureException('Signature could not get verified.');
}
$expectedHashes = json_decode($expectedHashes, true);
// Fixes for the updater as shipped with ownCloud 9.0.x: The updater is
// replaced after the code integrity check is performed.
//

View File

@ -899,7 +899,6 @@ class Server extends ServerContainer implements IServerContainer {
$config,
$c->getMemCacheFactory(),
$appManager,
$c->getTempManager(),
$c->getMimeTypeDetector()
);
});

View File

@ -77,12 +77,11 @@ class CheckerTest extends TestCase {
$this->config,
$this->cacheFactory,
$this->appManager,
\OC::$server->getTempManager(),
$this->mimeTypeDetector
);
}
public function testWriteAppSignatureOfNotExistingApp() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Exception message');
@ -107,7 +106,7 @@ class CheckerTest extends TestCase {
$this->checker->writeAppSignature('NotExistingApp', $x509, $rsa);
}
public function testWriteAppSignatureWrongPermissions() {
$this->expectException(\Exception::class);
$this->expectExceptionMessageRegExp('/[a-zA-Z\\/_-]+ is not writable/');
@ -480,7 +479,7 @@ class CheckerTest extends TestCase {
$this->assertSame([], $this->checker->verifyAppSignature('SomeApp'));
}
public function testWriteCoreSignatureWithException() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Exception message');
@ -504,7 +503,7 @@ class CheckerTest extends TestCase {
$this->checker->writeCoreSignature($x509, $rsa, __DIR__);
}
public function testWriteCoreSignatureWrongPermissions() {
$this->expectException(\Exception::class);
$this->expectExceptionMessageRegExp('/[a-zA-Z\\/_-]+ is not writable/');