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>
This commit is contained in:
parent
20ce9e89c1
commit
ac27507f0f
|
@ -215,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,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -239,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)
|
||||
|
@ -261,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
|
||||
*
|
||||
|
@ -271,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)
|
||||
|
@ -316,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'];
|
||||
|
||||
|
@ -343,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.
|
||||
//
|
||||
|
|
|
@ -106,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/');
|
||||
|
@ -479,7 +479,7 @@ class CheckerTest extends TestCase {
|
|||
$this->assertSame([], $this->checker->verifyAppSignature('SomeApp'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testWriteCoreSignatureWithException() {
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Exception message');
|
||||
|
@ -503,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/');
|
||||
|
|
Loading…
Reference in New Issue