Skip to content

Commit

Permalink
chore: validate signature by resource
Browse files Browse the repository at this point in the history
Signed-off-by: Vitor Mattos <[email protected]>
  • Loading branch information
vitormattos committed Jun 4, 2024
1 parent 128480d commit f55d4ce
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 58 deletions.
49 changes: 33 additions & 16 deletions lib/Service/Install/ConfigureCheckService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
use OCP\AppFramework\Services\IAppConfig;

class ConfigureCheckService {
private string $architecture;
public function __construct(
private IAppConfig $appConfig,
private SystemConfig $systemConfig,
private JSignPdfHandler $jSignPdfHandler,
private CertificateEngine $certificateEngine,
private InstallService $installService,
private SignSetupService $signSetupService,
) {
$this->architecture = php_uname('m');
}

/**
Expand All @@ -43,27 +45,12 @@ public function checkAll(): array {
*/
public function checkSign(): array {
$return = [];
$return = array_merge($return, $this->checkHash());
$return = array_merge($return, $this->checkJava());
$return = array_merge($return, $this->checkPdftk());
$return = array_merge($return, $this->checkJSignPdf());
return $return;
}

public function checkHash(): array {
if (!$this->installService->isDownloadedFilesOk()) {
return [
(new ConfigureCheckHelper())
->setErrorMessage(
'Invalid hash of binaries files.'
)
->setResource('java')
->setTip('Run occ libresign:install --all'),
];
}
return [];
}

/**
* Check all requirements to use JSignPdf
*
Expand All @@ -72,6 +59,16 @@ public function checkHash(): array {
public function checkJSignPdf(): array {
$jsignpdJarPath = $this->appConfig->getAppValue('jsignpdf_jar_path');
if ($jsignpdJarPath) {
if (count($this->signSetupService->verify($this->architecture, 'jsignpdf'))) {
return [
(new ConfigureCheckHelper())
->setErrorMessage(
'Invalid hash of binaries files.'
)
->setResource('jsignpdf')
->setTip('Run occ libresign:install --all'),
];
}
if (file_exists($jsignpdJarPath)) {
if (!$this->isJavaOk()) {
return [
Expand Down Expand Up @@ -132,6 +129,16 @@ public function checkJSignPdf(): array {
public function checkPdftk(): array {
$pdftkPath = $this->appConfig->getAppValue('pdftk_path');
if ($pdftkPath) {
if (count($this->signSetupService->verify($this->architecture, 'pdftk'))) {
return [
(new ConfigureCheckHelper())
->setErrorMessage(
'Invalid hash of binaries files.'
)
->setResource('pdftk')
->setTip('Run occ libresign:install --all'),
];
}
if (file_exists($pdftkPath)) {
if (!$this->isJavaOk()) {
return [
Expand Down Expand Up @@ -201,6 +208,16 @@ public function checkPdftk(): array {
private function checkJava(): array {
$javaPath = $this->appConfig->getAppValue('java_path');
if ($javaPath) {
if (count($this->signSetupService->verify($this->architecture, 'java'))) {
return [
(new ConfigureCheckHelper())
->setErrorMessage(
'Invalid hash of binaries files.'
)
->setResource('java')
->setTip('Run occ libresign:install --all'),
];
}
if (file_exists($javaPath)) {
\exec($javaPath . " -version 2>&1", $javaVersion, $resultCode);
if (empty($javaVersion)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Install/InstallService.php
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ public function setResource(string $resource): self {

public function isDownloadedFilesOk(): bool {
try {
return count($this->signSetupService->verify($this->architecture)) === 0;
return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0;
} catch (InvalidSignatureException $e) {
return false;
}
Expand Down
89 changes: 63 additions & 26 deletions lib/Service/Install/SignSetupService.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class SignSetupService {
'unauthetnicated',
];
private string $architecture;
private string $resource;
private array $signatureData = [];
private ?x509 $x509 = null;
public function __construct(
private EnvironmentHelper $environmentHelper,
private FileAccessHelper $fileAccessHelper,
Expand Down Expand Up @@ -59,14 +62,11 @@ public function writeAppSignature(
X509 $certificate,
RSA $privateKey,
string $architecture,
string $appInfoDir = '',
) {
$this->architecture = $architecture;
$appInfoDir = $this->getAppInfoDirectory($appInfoDir);
$appInfoDir = $this->getAppInfoDirectory();
try {
$this->fileAccessHelper->assertDirectoryExists($appInfoDir);

$iterator = $this->getFolderIterator();
$iterator = $this->getFolderIterator($this->getInstallPath());
$hashes = $this->generateHashes($iterator);
$signature = $this->createSignatureData($hashes, $certificate, $privateKey);
$this->fileAccessHelper->file_put_contents(
Expand All @@ -83,11 +83,10 @@ public function writeAppSignature(
}
}

private function getAppInfoDirectory(string $appInfoDir): string {
if (is_dir($appInfoDir)) {
return $appInfoDir;
}
return realpath(__DIR__ . '/../../../appinfo');
protected function getAppInfoDirectory(): string {
$appInfoDir = realpath(__DIR__ . '/../../../appinfo');
$this->fileAccessHelper->assertDirectoryExists($appInfoDir);
return $appInfoDir;
}

/**
Expand All @@ -102,10 +101,11 @@ private function splitCerts(string $cert): array {
return $matches[0];
}

public function verify(string $architecture, string $appInfoDir = '', string $certificateCN = Application::APP_ID): array {
$this->architecture = $architecture;
$appInfoDir = $this->getAppInfoDirectory($appInfoDir);

private function getSignatureData(): array {
if (!empty($this->signatureData)) {
return $this->signatureData;
}
$appInfoDir = $this->getAppInfoDirectory();
$signaturePath = $appInfoDir . '/install-' . $this->architecture . '.json';
$content = $this->fileAccessHelper->file_get_contents($signaturePath);
$signatureData = null;
Expand All @@ -116,45 +116,82 @@ public function verify(string $architecture, string $appInfoDir = '', string $ce
if (!\is_array($signatureData)) {
throw new InvalidSignatureException('Signature data not found.');
}
$this->signatureData = $signatureData;

$this->validateIfIssignedByLibresignAppCertificate($signatureData['hashes']);

return $this->signatureData;
}

private function getHashesOfResource(): array {
$signatureData = $this->getSignatureData();
$expectedHashes = $signatureData['hashes'];
ksort($expectedHashes);
$signature = base64_decode($signatureData['signature']);
$filtered = array_filter($expectedHashes, function (string $key) {
return str_starts_with($key, $this->resource);
}, ARRAY_FILTER_USE_KEY);
if (!$filtered) {
throw new InvalidSignatureException('No signature files to ' . $this->resource);
}
return $filtered;
}

private function getLibresignAppCertificate(): X509 {
if ($this->x509 instanceof X509) {
return $this->x509;
}
$signatureData = $this->getSignatureData();
$certificate = $signatureData['certificate'];

// Check if certificate is signed by Nextcloud Root Authority
$x509 = new \phpseclib\File\X509();
$this->x509 = new X509();
$rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');

$rootCerts = $this->splitCerts($rootCertificatePublicKey);
foreach ($rootCerts as $rootCert) {
$x509->loadCA($rootCert);
$this->x509->loadCA($rootCert);
}
$x509->loadX509($certificate);
if (!$x509->validateSignature()) {
$this->x509->loadX509($certificate);
if (!$this->x509->validateSignature()) {
throw new InvalidSignatureException('Certificate is not valid.');
}

// Verify if certificate has proper CN. "core" CN is always trusted.
if ($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
if ($this->x509->getDN(X509::DN_OPENSSL)['CN'] !== Application::APP_ID && $this->x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
throw new InvalidSignatureException(
sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', Application::APP_ID, $this->x509->getDN(true)['CN'])
);
}

return $this->x509;
}

private function validateIfIssignedByLibresignAppCertificate(array $expectedHashes): void {
$x509 = $this->getLibresignAppCertificate();

// Check if the signature of the files is valid
$rsa = new \phpseclib\Crypt\RSA();
$rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
$rsa->setMGFHash('sha512');
// See https://tools.ietf.org/html/rfc3447#page-38
$rsa->setSaltLength(0);

$signatureData = $this->getSignatureData();
$signature = base64_decode($signatureData['signature']);
if (!$rsa->verify(json_encode($expectedHashes), $signature)) {
throw new InvalidSignatureException('Signature could not get verified.');
}
}

public function verify(string $architecture, $resource): array {
$this->architecture = $architecture;
$this->resource = $resource;

$expectedHashes = $this->getHashesOfResource();

// Compare the list of files which are not identical
$installPath = $this->getInstallPath();
$currentInstanceHashes = $this->generateHashes($this->getFolderIterator(), $installPath);
$installPath = $this->getInstallPath() . '/' . $this->resource;
$currentInstanceHashes = $this->generateHashes($this->getFolderIterator($installPath), $installPath);
$differencesA = array_diff($expectedHashes, $currentInstanceHashes);
$differencesB = array_diff($currentInstanceHashes, $expectedHashes);
$differences = array_merge($differencesA, $differencesB);
Expand Down Expand Up @@ -224,9 +261,9 @@ private function getInstallPath(): string {
* @return \RecursiveIteratorIterator
* @throws \Exception
*/
private function getFolderIterator(): \RecursiveIteratorIterator {
private function getFolderIterator(string $folderToIterate): \RecursiveIteratorIterator {
$dirItr = new \RecursiveDirectoryIterator(
$this->getInstallPath(),
$folderToIterate,
\RecursiveDirectoryIterator::SKIP_DOTS
);

Expand Down
39 changes: 25 additions & 14 deletions tests/Unit/Service/Install/SignSetupServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private function getNewCert(): array {
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);

$csrNames = ['commonName' => 'LibreSign'];
$csrNames = ['commonName' => 'libresign'];

$csr = openssl_csr_new($csrNames, $privateKey, ['digest_alg' => 'sha256']);
$x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365, ['digest_alg' => 'sha256']);
Expand Down Expand Up @@ -115,8 +115,10 @@ private function writeAppSignature(string $architecture): SignSetupService {
$structure = [
'data' => [
'libresign' => [
'fakeFile01' => 'content',
'fakeFile02' => 'content',
'java' => [
'fakeFile01' => 'content',
'fakeFile02' => 'content',
],
],
],
'resources' => [
Expand All @@ -134,18 +136,25 @@ private function writeAppSignature(string $architecture): SignSetupService {
$this->environmentHelper->method('getServerRoot')
->willReturn('vfs://home');

$signSetupService = $this->getInstance(['getInternalPathOfFolder']);
$signSetupService = $this->getInstance([
'getInternalPathOfFolder',
'getAppInfoDirectory',
]);
$signSetupService->expects($this->any())
->method('getInternalPathOfFolder')
->willReturn('libresign');
$signSetupService->expects($this->any())
->method('getAppInfoDirectory')
->willReturn('vfs://home/appinfo');

$signSetupService->writeAppSignature($x509, $rsa, $architecture, 'vfs://home/appinfo');
$this->assertFileExists('vfs://home/appinfo/install-' . $architecture . '.json');
$json = file_get_contents('vfs://home/appinfo/install-' . $architecture . '.json');
$signatureContent = json_decode($json, true);
$this->assertArrayHasKey('hashes', $signatureContent);
$this->assertCount(2, $signatureContent['hashes']);
$expected = hash('sha512', $structure['data']['libresign']['fakeFile01']);
$actual = $signatureContent['hashes']['fakeFile01'];
$expected = hash('sha512', $structure['data']['libresign']['java']['fakeFile01']);
$actual = $signatureContent['hashes']['java/fakeFile01'];
$this->assertEquals($expected, $actual);
return $signSetupService;
}
Expand All @@ -155,7 +164,9 @@ private function writeAppSignature(string $architecture): SignSetupService {
*/
public function testWriteAppSignature(string $architecture): void {
$signSetupService = $this->writeAppSignature($architecture);
$actual = $signSetupService->verify($architecture, 'vfs://home/appinfo', 'LibreSign');
$architecture = 'x86_64';
$resource = 'java';
$actual = $signSetupService->verify($architecture, $resource);
$this->assertCount(0, $actual);
}

Expand All @@ -168,30 +179,30 @@ public static function dataWriteAppSignature(): array {
public function testVerify(): void {
$architecture = 'x86_64';
$signSetupService = $this->writeAppSignature($architecture);
unlink('vfs://home/data/libresign/fakeFile01');
file_put_contents('vfs://home/data/libresign/fakeFile02', 'invalidContent');
file_put_contents('vfs://home/data/libresign/fakeFile03', 'invalidContent');
unlink('vfs://home/data/libresign/java/fakeFile01');
file_put_contents('vfs://home/data/libresign/java/fakeFile02', 'invalidContent');
file_put_contents('vfs://home/data/libresign/java/fakeFile03', 'invalidContent');
$expected = json_encode([
'FILE_MISSING' => [
'fakeFile01' => [
'java/fakeFile01' => [
'expected' => 'b2d1d285b5199c85f988d03649c37e44fd3dde01e5d69c50fef90651962f48110e9340b60d49a479c4c0b53f5f07d690686dd87d2481937a512e8b85ee7c617f',
'current' => '',
],
],
'INVALID_HASH' => [
'fakeFile02' => [
'java/fakeFile02' => [
'expected' => 'b2d1d285b5199c85f988d03649c37e44fd3dde01e5d69c50fef90651962f48110e9340b60d49a479c4c0b53f5f07d690686dd87d2481937a512e8b85ee7c617f',
'current' => '827a4e298c978e1eeffebdf09f0fa5a1e1d8b608c8071144f3fffb31f9ed21f6d27f88a63f7409583df7438105f713ff58d55e68e61e01a285125d763045c726',
],
],
'EXTRA_FILE' => [
'fakeFile03' => [
'java/fakeFile03' => [
'expected' => '',
'current' => '827a4e298c978e1eeffebdf09f0fa5a1e1d8b608c8071144f3fffb31f9ed21f6d27f88a63f7409583df7438105f713ff58d55e68e61e01a285125d763045c726',
],
],
]);
$actual = $signSetupService->verify($architecture, 'vfs://home/appinfo', 'LibreSign');
$actual = $signSetupService->verify($architecture, 'java');
$actual = json_encode($actual);
$this->assertJsonStringEqualsJsonString($expected, $actual);
}
Expand Down
3 changes: 2 additions & 1 deletion tests/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
</file>
<file src="lib/Service/Install/SignSetupService.php">
<UndefinedClass>
<code><![CDATA[$x509->currentCert]]></code>
<code><![CDATA[?x509]]></code>
<code><![CDATA[RSA]]></code>
<code><![CDATA[RSA]]></code>
<code><![CDATA[RSA]]></code>
Expand All @@ -136,7 +138,6 @@
<code><![CDATA[X509]]></code>
<code><![CDATA[X509]]></code>
<code><![CDATA[\phpseclib\Crypt\RSA]]></code>
<code><![CDATA[\phpseclib\File\X509]]></code>
</UndefinedClass>
</file>
<file src="lib/Service/RequestSignatureService.php">
Expand Down

0 comments on commit f55d4ce

Please sign in to comment.