From 385fe624a1e90eb9dbaed92fc3b989b73b6cf7d8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 11 Jun 2024 16:55:36 -0300 Subject: [PATCH 01/11] fix: turn sign setup optional at development environment Signed-off-by: Vitor Mattos --- lib/Command/Developer/SignSetup.php | 23 ++- lib/Command/Install.php | 18 ++ lib/Exception/EmptySignatureDataException.php | 12 ++ .../SignatureDataNotFoundException.php | 12 ++ lib/Service/Install/ConfigureCheckService.php | 23 ++- lib/Service/Install/InstallService.php | 174 +++++++++--------- lib/Service/Install/SignSetupService.php | 38 ++-- .../Service/Install/SignSetupServiceTest.php | 50 ++--- 8 files changed, 220 insertions(+), 130 deletions(-) create mode 100644 lib/Exception/EmptySignatureDataException.php create mode 100644 lib/Exception/SignatureDataNotFoundException.php diff --git a/lib/Command/Developer/SignSetup.php b/lib/Command/Developer/SignSetup.php index 31606b870e..53f958047a 100644 --- a/lib/Command/Developer/SignSetup.php +++ b/lib/Command/Developer/SignSetup.php @@ -34,21 +34,22 @@ public function isEnabled(): bool { protected function configure(): void { $this ->setName('libresign:developer:sign-setup') - ->setDescription('Clean all LibreSign data') + ->setDescription('Sign the current setup') + ->addOption('develop', null, InputOption::VALUE_NONE, 'If develop mode enabled, do not will be necessary to use privateKey and certificate.') ->addOption('privateKey', null, InputOption::VALUE_REQUIRED, 'Path to private key to use for signing') ->addOption('certificate', null, InputOption::VALUE_REQUIRED, 'Path to certificate to use for signing') ; } protected function execute(InputInterface $input, OutputInterface $output): int { + $this->handleDevelopMode($input); $privateKeyPath = $input->getOption('privateKey'); $keyBundlePath = $input->getOption('certificate'); if (is_null($privateKeyPath) || is_null($keyBundlePath)) { - $output->writeln('This command requires the --path, --privateKey and --certificate.'); + $output->writeln('This command requires --privateKey and --certificate.'); $output->writeln('Example: ./occ libresign:developer:sign-setup --privateKey="/libresign/private/myapp.key" --certificate="/libresign/public/mycert.crt"'); return 1; } - $privateKey = $this->fileAccessHelper->file_get_contents((string) $privateKeyPath); $keyBundle = $this->fileAccessHelper->file_get_contents((string) $keyBundlePath); if ($privateKey === false) { @@ -77,4 +78,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int } return 0; } + + private function handleDevelopMode(InputInterface $input): void { + $develop = $input->getOption('develop'); + if (!$develop) { + return; + } + if (file_exists(__DIR__ . '/../../../build/tools/certificates/libresign.crt')) { + if (!$input->getOption('privateKey')) { + $input->setOption('certificate', __DIR__ . '/../../../build/tools/certificates/libresign.crt'); + } + if (!$input->getOption('certificate')) { + $input->setOption('privateKey', __DIR__ . '/../../../build/tools/certificates/libresign.key'); + } + return; + } + } } diff --git a/lib/Command/Install.php b/lib/Command/Install.php index 18b07b3006..d82b56b0c6 100644 --- a/lib/Command/Install.php +++ b/lib/Command/Install.php @@ -8,11 +8,22 @@ namespace OCA\Libresign\Command; +use OCA\Libresign\Service\Install\InstallService; +use OCP\AppFramework\Services\IAppConfig; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Install extends Base { + public function __construct( + InstallService $installService, + LoggerInterface $logger, + private IAppConfig $appConfig, + ) { + parent::__construct($installService, $logger); + } + protected function configure(): void { $this ->setName('libresign:install') @@ -78,7 +89,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $ok = true; } if ($input->getOption('cfssl') || $all) { + $currentEngine = $this->appConfig->getAppValue('certificate_engine', 'openssl'); + if ($currentEngine !== 'cfssl') { + $this->appConfig->setAppValue('certificate_engine', 'cfssl'); + } $this->installService->installCfssl(); + if ($currentEngine !== 'cfssl') { + $this->appConfig->setAppValue('certificate_engine', $currentEngine); + } $ok = true; } } catch (\Exception $e) { diff --git a/lib/Exception/EmptySignatureDataException.php b/lib/Exception/EmptySignatureDataException.php new file mode 100644 index 0000000000..4b79b39711 --- /dev/null +++ b/lib/Exception/EmptySignatureDataException.php @@ -0,0 +1,12 @@ +appConfig->getAppValue('jsignpdf_jar_path'); if ($jsignpdJarPath) { - if (count($this->signSetupService->verify($this->architecture, 'jsignpdf'))) { + if (count($this->verify('jsignpdf'))) { return [ (new ConfigureCheckHelper()) ->setErrorMessage( @@ -129,7 +129,7 @@ public function checkJSignPdf(): array { public function checkPdftk(): array { $pdftkPath = $this->appConfig->getAppValue('pdftk_path'); if ($pdftkPath) { - if (count($this->signSetupService->verify($this->architecture, 'pdftk'))) { + if (count($this->verify('pdftk'))) { return [ (new ConfigureCheckHelper()) ->setErrorMessage( @@ -200,6 +200,23 @@ public function checkPdftk(): array { ]; } + public function isDebugEnabled(): bool { + return $this->systemConfig->getValue('debug', false) === true; + } + + private function verify(string $resource): array { + $result = $this->signSetupService->verify($this->architecture, $resource); + if (count($result) === 1 && $this->isDebugEnabled()) { + if (isset($result['SIGNATURE_DATA_NOT_FOUND'])) { + return []; + } + if (isset($result['EMPTY_SIGNATURE_DATA'])) { + return []; + } + } + return $result; + } + /** * Check all requirements to use Java * @@ -208,7 +225,7 @@ public function checkPdftk(): array { private function checkJava(): array { $javaPath = $this->appConfig->getAppValue('java_path'); if ($javaPath) { - if (count($this->signSetupService->verify($this->architecture, 'java'))) { + if (count($this->verify('java'))) { return [ (new ConfigureCheckHelper()) ->setErrorMessage( diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index e4fdf3e0dc..67247aeab1 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -14,8 +14,10 @@ use OC\Archive\ZIP; use OC\Memcache\NullCache; use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Exception\EmptySignatureDataException; use OCA\Libresign\Exception\InvalidSignatureException; use OCA\Libresign\Exception\LibresignException; +use OCA\Libresign\Exception\SignatureDataNotFoundException; use OCA\Libresign\Handler\CertificateEngine\AEngineHandler; use OCA\Libresign\Handler\CertificateEngine\CfsslHandler; use OCA\Libresign\Handler\CertificateEngine\Handler as CertificateEngineHandler; @@ -340,16 +342,13 @@ public function setResource(string $resource): self { public function isDownloadedFilesOk(): bool { try { return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0; - } catch (InvalidSignatureException $e) { + } catch (InvalidSignatureException|SignatureDataNotFoundException|EmptySignatureDataException|NotFoundException $e) { return false; } } public function installJava(?bool $async = false): void { $this->setResource('java'); - if ($this->isDownloadedFilesOk()) { - return; - } if ($async) { $this->runAsync(); return; @@ -357,41 +356,43 @@ public function installJava(?bool $async = false): void { $folder = $this->getEmptyFolder($this->resource); $extractDir = $this->getFullPath() . '/' . $this->resource; - /** - * Steps to update: - * Check the compatible version of Java to use JSignPdf - * Update all the follow data - * Update the constants with java version - * URL used to get the MD5 and URL to download: - * https://jdk.java.net/java-se-ri/8-MR3 - */ - if (PHP_OS_FAMILY === 'Linux') { - $linuxDistribution = $this->getLinuxDistributionToDownloadJava(); - if ($this->architecture === 'x86_64') { - $compressedFileName = 'OpenJDK21U-jre_x64_' . $linuxDistribution . '_hotspot_' . self::JAVA_PARTIAL_VERSION . '.tar.gz'; - $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName; - } elseif ($this->architecture === 'aarch64') { - $compressedFileName = 'OpenJDK21U-jre_aarch64_' . $linuxDistribution . '_hotspot_' . self::JAVA_PARTIAL_VERSION . '.tar.gz'; - $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName; + if (!$this->isDownloadedFilesOk()) { + /** + * Steps to update: + * Check the compatible version of Java to use JSignPdf + * Update all the follow data + * Update the constants with java version + * URL used to get the MD5 and URL to download: + * https://jdk.java.net/java-se-ri/8-MR3 + */ + if (PHP_OS_FAMILY === 'Linux') { + $linuxDistribution = $this->getLinuxDistributionToDownloadJava(); + if ($this->architecture === 'x86_64') { + $compressedFileName = 'OpenJDK21U-jre_x64_' . $linuxDistribution . '_hotspot_' . self::JAVA_PARTIAL_VERSION . '.tar.gz'; + $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName; + } elseif ($this->architecture === 'aarch64') { + $compressedFileName = 'OpenJDK21U-jre_aarch64_' . $linuxDistribution . '_hotspot_' . self::JAVA_PARTIAL_VERSION . '.tar.gz'; + $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName; + } + $class = TAR::class; + } else { + throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY)); } - $class = TAR::class; - } else { - throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY)); - } - $checksumUrl = $url . '.sha256.txt'; - $hash = $this->getHash($compressedFileName, $checksumUrl); - try { - $compressedFile = $folder->getFile($compressedFileName); - } catch (NotFoundException $th) { - $compressedFile = $folder->newFile($compressedFileName); - } - $comporessedInternalFileName = $this->getDataDir() . '/' . $this->getInternalPathOfFile($compressedFile); + $checksumUrl = $url . '.sha256.txt'; + $hash = $this->getHash($compressedFileName, $checksumUrl); + try { + $compressedFile = $folder->getFile($compressedFileName); + } catch (NotFoundException $th) { + $compressedFile = $folder->newFile($compressedFileName); + } + $comporessedInternalFileName = $this->getDataDir() . '/' . $this->getInternalPathOfFile($compressedFile); - $this->download($url, 'java', $comporessedInternalFileName, $hash, 'sha256'); + $this->download($url, 'java', $comporessedInternalFileName, $hash, 'sha256'); - $extractor = new $class($comporessedInternalFileName); - $extractor->extract($extractDir); - unlink($comporessedInternalFileName); + $extractor = new $class($comporessedInternalFileName); + $extractor->extract($extractDir); + unlink($comporessedInternalFileName); + } $this->appConfig->setAppValue('java_path', $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java'); $this->removeDownloadProgress(); @@ -428,9 +429,6 @@ public function installJSignPdf(?bool $async = false): void { throw new RuntimeException('Zip extension is not available'); } $this->setResource('jsignpdf'); - if ($this->isDownloadedFilesOk()) { - return; - } if ($async) { $this->runAsync(); return; @@ -438,22 +436,24 @@ public function installJSignPdf(?bool $async = false): void { $folder = $this->getEmptyFolder($this->resource); $extractDir = $this->getFullPath() . '/' . $this->resource; - $compressedFileName = 'jsignpdf-' . JSignPdfHandler::VERSION . '.zip'; - try { - $compressedFile = $folder->getFile($compressedFileName); - } catch (\Throwable $th) { - $compressedFile = $folder->newFile($compressedFileName); - } - $comporessedInternalFileName = $this->getDataDir() . '/' . $this->getInternalPathOfFile($compressedFile); - $url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', JSignPdfHandler::VERSION) . '/jsignpdf-' . JSignPdfHandler::VERSION . '.zip'; - /** WHEN UPDATE version: generate this hash handmade and update here */ - $hash = '7c66f5a9f5e7e35b601725414491a867'; + if (!$this->isDownloadedFilesOk()) { + $compressedFileName = 'jsignpdf-' . JSignPdfHandler::VERSION . '.zip'; + try { + $compressedFile = $folder->getFile($compressedFileName); + } catch (\Throwable $th) { + $compressedFile = $folder->newFile($compressedFileName); + } + $comporessedInternalFileName = $this->getDataDir() . '/' . $this->getInternalPathOfFile($compressedFile); + $url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', JSignPdfHandler::VERSION) . '/jsignpdf-' . JSignPdfHandler::VERSION . '.zip'; + /** WHEN UPDATE version: generate this hash handmade and update here */ + $hash = '7c66f5a9f5e7e35b601725414491a867'; - $this->download($url, 'JSignPdf', $comporessedInternalFileName, $hash); + $this->download($url, 'JSignPdf', $comporessedInternalFileName, $hash); - $zip = new ZIP($extractDir . '/' . $compressedFileName); - $zip->extract($extractDir); - unlink($extractDir . '/' . $compressedFileName); + $zip = new ZIP($extractDir . '/' . $compressedFileName); + $zip->extract($extractDir); + unlink($extractDir . '/' . $compressedFileName); + } $fullPath = $extractDir . '/jsignpdf-' . JSignPdfHandler::VERSION . '/JSignPdf.jar'; $this->appConfig->setAppValue('jsignpdf_jar_path', $fullPath); @@ -476,9 +476,6 @@ public function uninstallJSignPdf(): void { public function installPdftk(?bool $async = false): void { $this->setResource('pdftk'); - if ($this->isDownloadedFilesOk()) { - return; - } if ($async) { $this->runAsync(); return; @@ -490,11 +487,14 @@ public function installPdftk(?bool $async = false): void { $file = $folder->newFile('pdftk.jar'); } $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file); - $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar'; - /** WHEN UPDATE version: generate this hash handmade and update here */ - $hash = '59a28bed53b428595d165d52988bf4cf'; - $this->download($url, 'pdftk', $fullPath, $hash); + if (!$this->isDownloadedFilesOk()) { + $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar'; + /** WHEN UPDATE version: generate this hash handmade and update here */ + $hash = '59a28bed53b428595d165d52988bf4cf'; + + $this->download($url, 'pdftk', $fullPath, $hash); + } $this->appConfig->setAppValue('pdftk_path', $fullPath); $this->removeDownloadProgress(); @@ -522,9 +522,6 @@ public function installCfssl(?bool $async = false): void { return; } $this->setResource('cfssl'); - if ($this->isDownloadedFilesOk()) { - return; - } if ($async) { $this->runAsync(); return; @@ -545,27 +542,29 @@ public function installCfssl(?bool $async = false): void { private function installCfsslByArchitecture(string $arcitecture): void { $folder = $this->getEmptyFolder($this->resource); - $downloads = [ - [ - 'file' => 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $arcitecture, - 'destination' => 'cfssl', - ], - [ - 'file' => 'cfssljson_' . self::CFSSL_VERSION . '_linux_' . $arcitecture, - 'destination' => 'cfssljson', - ], - ]; - $baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/'; - $checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt'; - foreach ($downloads as $download) { - $hash = $this->getHash($download['file'], $checksumUrl); - - $file = $folder->newFile($download['destination']); - $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file); - - $this->download($baseUrl . $download['file'], $download['destination'], $fullPath, $hash, 'sha256'); - - chmod($fullPath, 0700); + if ($this->isDownloadedFilesOk()) { + $downloads = [ + [ + 'file' => 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $arcitecture, + 'destination' => 'cfssl', + ], + [ + 'file' => 'cfssljson_' . self::CFSSL_VERSION . '_linux_' . $arcitecture, + 'destination' => 'cfssljson', + ], + ]; + $baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/'; + $checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt'; + foreach ($downloads as $download) { + $hash = $this->getHash($download['file'], $checksumUrl); + + $file = $folder->newFile($download['destination']); + $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file); + + $this->download($baseUrl . $download['file'], $download['destination'], $fullPath, $hash, 'sha256'); + + chmod($fullPath, 0700); + } } $cfsslBinPath = $this->getDataDir() . '/' . @@ -639,12 +638,15 @@ protected function downloadCli(string $url, string $filename, string $path, ?str }, ]); } catch (\Exception $e) { + $progressBar->finish(); + $this->output->writeln(''); $this->output->writeln('Failure on download ' . $filename . ' try again.'); $this->output->writeln('' . $e->getMessage() . ''); $this->logger->error('Failure on download ' . $filename . '. ' . $e->getMessage()); + } finally { + $progressBar->finish(); + $this->output->writeln(''); } - $progressBar->finish(); - $this->output->writeln(''); if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) { $this->output->writeln('Failure on download ' . $filename . ' try again'); $this->output->writeln('Invalid ' . $hash_algo . ''); diff --git a/lib/Service/Install/SignSetupService.php b/lib/Service/Install/SignSetupService.php index 228c3529cd..8dec9620c1 100644 --- a/lib/Service/Install/SignSetupService.php +++ b/lib/Service/Install/SignSetupService.php @@ -11,7 +11,9 @@ use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Exception\EmptySignatureDataException; use OCA\Libresign\Exception\InvalidSignatureException; +use OCA\Libresign\Exception\SignatureDataNotFoundException; use OCP\App\IAppManager; use OCP\Files\AppData\IAppDataFactory; use OCP\Files\IAppData; @@ -114,7 +116,7 @@ private function getSignatureData(): array { $signatureData = json_decode($content, true); } if (!\is_array($signatureData)) { - throw new InvalidSignatureException('Signature data not found.'); + throw new SignatureDataNotFoundException('Signature data not found.'); } $this->signatureData = $signatureData; @@ -130,7 +132,7 @@ private function getHashesOfResource(): array { return str_starts_with($key, $this->resource); }, ARRAY_FILTER_USE_KEY); if (!$filtered) { - throw new InvalidSignatureException('No signature files to ' . $this->resource); + throw new EmptySignatureDataException('No signature files to ' . $this->resource); } return $filtered; } @@ -187,11 +189,25 @@ public function verify(string $architecture, $resource): array { $this->architecture = $architecture; $this->resource = $resource; - $expectedHashes = $this->getHashesOfResource(); + try { + $expectedHashes = $this->getHashesOfResource(); + // Compare the list of files which are not identical + $installPath = $this->getInstallPath() . '/' . $this->resource; + $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($installPath), $installPath); + } catch (EmptySignatureDataException $th) { + return [ + 'EMPTY_SIGNATURE_DATA' => $th->getMessage(), + ]; + } catch (SignatureDataNotFoundException $th) { + return [ + 'SIGNATURE_DATA_NOT_FOUND' => $th->getMessage(), + ]; + } catch (\Throwable $th) { + return [ + 'HASH_FILE_ERROR' => $th->getMessage(), + ]; + } - // Compare the list of files which are not identical - $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); @@ -243,12 +259,8 @@ protected function getInternalPathOfFolder(ISimpleFolder $node): string { } private function getInstallPath(): string { - try { - $folder = $this->getDataDir() . '/' . - $this->getInternalPathOfFolder($this->appData->getFolder($this->architecture)); - } catch (NotFoundException $e) { - throw new InvalidSignatureException('Invalid architecture ' . $this->architecture); - } + $folder = $this->getDataDir() . '/' . + $this->getInternalPathOfFolder($this->appData->getFolder($this->architecture)); return $folder; } @@ -263,7 +275,7 @@ private function getInstallPath(): string { */ private function getFolderIterator(string $folderToIterate): \RecursiveIteratorIterator { if (!is_dir($folderToIterate)) { - throw new InvalidSignatureException('No such directory ' . $folderToIterate); + throw new NotFoundException('No such directory ' . $folderToIterate); } $dirItr = new \RecursiveDirectoryIterator( $folderToIterate, diff --git a/tests/Unit/Service/Install/SignSetupServiceTest.php b/tests/Unit/Service/Install/SignSetupServiceTest.php index ff933e2001..3895518bca 100644 --- a/tests/Unit/Service/Install/SignSetupServiceTest.php +++ b/tests/Unit/Service/Install/SignSetupServiceTest.php @@ -50,7 +50,7 @@ private function getInstance(array $methods = []) { ->getMock(); } - private function getNewCert(): array { + public function getDevelopCert(): array { $privateKey = openssl_pkey_new([ 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, @@ -62,16 +62,16 @@ private function getNewCert(): array { $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365, ['digest_alg' => 'sha256']); openssl_x509_export($x509, $rootCertificate); - openssl_pkey_export($privateKey, $publicKey); + openssl_pkey_export($privateKey, $privateKeyCert); - $privateKey = openssl_pkey_new([ + $privateKeyInstance = openssl_pkey_new([ 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ]); return [ - 'privateKey' => $privateKey, 'rootCertificate' => $rootCertificate, - 'publicKey' => $publicKey, + 'privateKeyInstance' => $privateKeyInstance, + 'privateKeyCert' => $privateKeyCert, ]; } @@ -101,13 +101,30 @@ public static function dataGetArchitectures(): array { } private function writeAppSignature(string $architecture): SignSetupService { + $this->config->method('getSystemValue') + ->willReturn(vfsStream::url('home/data')); + + $this->environmentHelper->method('getServerRoot') + ->willReturn('vfs://home'); + + $signSetupService = $this->getInstance([ + 'getInternalPathOfFolder', + 'getAppInfoDirectory', + ]); + $signSetupService->expects($this->any()) + ->method('getInternalPathOfFolder') + ->willReturn('libresign'); + $signSetupService->expects($this->any()) + ->method('getAppInfoDirectory') + ->willReturn('vfs://home/appinfo'); + $this->appManager->method('getAppInfo') ->willReturn(['dependencies' => ['architecture' => [$architecture]]]); - $certificate = $this->getNewCert('123456'); + $certificate = $this->getDevelopCert(); $rsa = new RSA(); - $rsa->loadKey($certificate['privateKey']); - $rsa->loadKey($certificate['publicKey']); + $rsa->loadKey($certificate['privateKeyInstance']); + $rsa->loadKey($certificate['privateKeyCert']); $x509 = new X509(); $x509->loadX509($certificate['rootCertificate']); $x509->setPrivateKey($rsa); @@ -130,23 +147,6 @@ private function writeAppSignature(string $architecture): SignSetupService { ]; $root = vfsStream::setup('home', null, $structure); - $this->config->method('getSystemValue') - ->willReturn(vfsStream::url('home/data')); - - $this->environmentHelper->method('getServerRoot') - ->willReturn('vfs://home'); - - $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'); From 29c7951d799d4e04bcaed377107dd2a716627aa2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 11 Jun 2024 17:16:58 -0300 Subject: [PATCH 02/11] chore: move the logic to don't install CFSSL to install command Signed-off-by: Vitor Mattos --- lib/Command/Install.php | 5 +---- lib/Service/Install/InstallService.php | 6 ------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/Command/Install.php b/lib/Command/Install.php index d82b56b0c6..7cd750a258 100644 --- a/lib/Command/Install.php +++ b/lib/Command/Install.php @@ -90,12 +90,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->getOption('cfssl') || $all) { $currentEngine = $this->appConfig->getAppValue('certificate_engine', 'openssl'); - if ($currentEngine !== 'cfssl') { - $this->appConfig->setAppValue('certificate_engine', 'cfssl'); - } $this->installService->installCfssl(); if ($currentEngine !== 'cfssl') { - $this->appConfig->setAppValue('certificate_engine', $currentEngine); + $output->writeln('To use CFSSL, set the engine to cfssl with: config:app:set libresign certificate_engine --value cfssl'); } $ok = true; } diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index 67247aeab1..bc3f3beaf9 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -515,12 +515,6 @@ public function uninstallPdftk(): void { } public function installCfssl(?bool $async = false): void { - if ($this->certificateEngineHandler->getEngine()->getName() !== 'cfssl') { - if (!$async) { - throw new InvalidArgumentException('Set the engine to cfssl with: config:app:set libresign certificate_engine --value cfssl'); - } - return; - } $this->setResource('cfssl'); if ($async) { $this->runAsync(); From 193c7c0ad4c31d973679ed25a9793c89f6b80066 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 12:34:16 -0300 Subject: [PATCH 03/11] fix: usage of verify method Previous commit made a wrong usage of verify method Signed-off-by: Vitor Mattos --- lib/Service/Install/InstallService.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index bc3f3beaf9..f4e9419494 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -99,7 +99,7 @@ private function getFolder(string $path = '', ?ISimpleFolder $folder = null, $ne } $path = explode('/', $path); foreach ($path as $snippet) { - $folder = $this->getFolder($snippet, $folder); + $folder = $this->getFolder($snippet, $folder, $needToBeEmpty); } return $folder; } @@ -340,11 +340,7 @@ public function setResource(string $resource): self { } public function isDownloadedFilesOk(): bool { - try { - return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0; - } catch (InvalidSignatureException|SignatureDataNotFoundException|EmptySignatureDataException|NotFoundException $e) { - return false; - } + return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0; } public function installJava(?bool $async = false): void { @@ -353,10 +349,12 @@ public function installJava(?bool $async = false): void { $this->runAsync(); return; } - $folder = $this->getEmptyFolder($this->resource); $extractDir = $this->getFullPath() . '/' . $this->resource; - if (!$this->isDownloadedFilesOk()) { + if ($this->isDownloadedFilesOk()) { + $folder = $this->getFolder($this->resource); + } else { + $folder = $this->getEmptyFolder($this->resource); /** * Steps to update: * Check the compatible version of Java to use JSignPdf @@ -433,10 +431,12 @@ public function installJSignPdf(?bool $async = false): void { $this->runAsync(); return; } - $folder = $this->getEmptyFolder($this->resource); $extractDir = $this->getFullPath() . '/' . $this->resource; - if (!$this->isDownloadedFilesOk()) { + if ($this->isDownloadedFilesOk()) { + $folder = $this->getFolder($this->resource); + } else { + $folder = $this->getEmptyFolder($this->resource); $compressedFileName = 'jsignpdf-' . JSignPdfHandler::VERSION . '.zip'; try { $compressedFile = $folder->getFile($compressedFileName); @@ -534,9 +534,10 @@ public function installCfssl(?bool $async = false): void { } private function installCfsslByArchitecture(string $arcitecture): void { - $folder = $this->getEmptyFolder($this->resource); - if ($this->isDownloadedFilesOk()) { + $folder = $this->getFolder($this->resource); + } else { + $folder = $this->getEmptyFolder($this->resource); $downloads = [ [ 'file' => 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $arcitecture, From 82717f8ee2316debd4efe26256f0c0df443534b7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 12:55:54 -0300 Subject: [PATCH 04/11] fix: cs Signed-off-by: Vitor Mattos --- lib/Service/Install/InstallService.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index f4e9419494..3ff8c54860 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -14,10 +14,7 @@ use OC\Archive\ZIP; use OC\Memcache\NullCache; use OCA\Libresign\AppInfo\Application; -use OCA\Libresign\Exception\EmptySignatureDataException; -use OCA\Libresign\Exception\InvalidSignatureException; use OCA\Libresign\Exception\LibresignException; -use OCA\Libresign\Exception\SignatureDataNotFoundException; use OCA\Libresign\Handler\CertificateEngine\AEngineHandler; use OCA\Libresign\Handler\CertificateEngine\CfsslHandler; use OCA\Libresign\Handler\CertificateEngine\Handler as CertificateEngineHandler; From 7afd824098e2767ea9d30886b9cac807f93997f6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 14:33:13 -0300 Subject: [PATCH 05/11] chore: formating output Signed-off-by: Vitor Mattos --- lib/Command/Install.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/Install.php b/lib/Command/Install.php index 7cd750a258..0162f90145 100644 --- a/lib/Command/Install.php +++ b/lib/Command/Install.php @@ -92,7 +92,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $currentEngine = $this->appConfig->getAppValue('certificate_engine', 'openssl'); $this->installService->installCfssl(); if ($currentEngine !== 'cfssl') { - $output->writeln('To use CFSSL, set the engine to cfssl with: config:app:set libresign certificate_engine --value cfssl'); + $output->writeln('To use CFSSL, set the engine to cfssl with: config:app:set libresign certificate_engine --value cfssl'); } $ok = true; } From f0f3196bfb7ce2e3cc9967b369cf4c3d0aed96ed Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 14:33:46 -0300 Subject: [PATCH 06/11] chore: sign and verify by resource Signed-off-by: Vitor Mattos --- lib/Command/Developer/SignSetup.php | 6 +++- lib/Service/Install/InstallService.php | 7 ++++- lib/Service/Install/SignSetupService.php | 38 +++++++++++++----------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/Command/Developer/SignSetup.php b/lib/Command/Developer/SignSetup.php index 53f958047a..85315762bc 100644 --- a/lib/Command/Developer/SignSetup.php +++ b/lib/Command/Developer/SignSetup.php @@ -10,6 +10,7 @@ use OC\Core\Command\Base; use OC\IntegrityCheck\Helpers\FileAccessHelper; +use OCA\Libresign\Service\Install\InstallService; use OCA\Libresign\Service\Install\SignSetupService; use OCP\IConfig; use phpseclib\Crypt\RSA; @@ -23,6 +24,7 @@ public function __construct( private IConfig $config, private FileAccessHelper $fileAccessHelper, private SignSetupService $signSetupService, + private InstallService $installService, ) { parent::__construct(); } @@ -69,7 +71,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $x509->setPrivateKey($rsa); try { foreach ($this->signSetupService->getArchitectures() as $architecture) { - $this->signSetupService->writeAppSignature($x509, $rsa, $architecture); + foreach ($this->installService->getAvailableResources() as $resource) { + $this->signSetupService->writeAppSignature($x509, $rsa, $architecture, $resource); + } } $output->writeln('Successfully signed'); } catch (\Exception $e) { diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index 3ff8c54860..dba60e9f82 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -102,8 +102,9 @@ private function getFolder(string $path = '', ?ISimpleFolder $folder = null, $ne } try { $folder = $folder->getFolder($path, $folder); - if ($needToBeEmpty) { + if ($needToBeEmpty && $path !== $this->architecture) { $folder->delete(); + $path = ''; throw new \Exception('Need to be empty'); } } catch (\Throwable $th) { @@ -259,6 +260,10 @@ private function removeCache(string $key): void { $this->cache->remove(Application::APP_ID . '-asyncDownloadProgress-' . $key); } + public function getAvailableResources(): array { + return $this->availableResources; + } + public function getTotalSize(): array { $return = []; foreach ($this->availableResources as $resource) { diff --git a/lib/Service/Install/SignSetupService.php b/lib/Service/Install/SignSetupService.php index 8dec9620c1..3e498e02c4 100644 --- a/lib/Service/Install/SignSetupService.php +++ b/lib/Service/Install/SignSetupService.php @@ -64,19 +64,26 @@ public function writeAppSignature( X509 $certificate, RSA $privateKey, string $architecture, + string $resource, ) { $this->architecture = $architecture; + $this->resource = $resource; $appInfoDir = $this->getAppInfoDirectory(); try { $iterator = $this->getFolderIterator($this->getInstallPath()); $hashes = $this->generateHashes($iterator); $signature = $this->createSignatureData($hashes, $certificate, $privateKey); $this->fileAccessHelper->file_put_contents( - $appInfoDir . '/install-' . $this->architecture . '.json', + $appInfoDir . '/install-' . $this->architecture . '-' . $this->resource . '.json', json_encode($signature, JSON_PRETTY_PRINT) ); } catch (NotFoundException $e) { - throw new \Exception(sprintf("Folder %s not found.\nIs necessary to run this command first: occ libresign:install --all --architecture %s", $e->getMessage(), $this->architecture)); + throw new \Exception(sprintf( + "Folder %s not found.\nIs necessary to run this command first: occ libresign:install --%s --architecture %s", + $e->getMessage(), + $this->resource, + $this->architecture, + )); } catch (\Exception $e) { if (!$this->fileAccessHelper->is_writable($appInfoDir)) { throw new \Exception($appInfoDir . ' is not writable'); @@ -108,7 +115,7 @@ private function getSignatureData(): array { return $this->signatureData; } $appInfoDir = $this->getAppInfoDirectory(); - $signaturePath = $appInfoDir . '/install-' . $this->architecture . '.json'; + $signaturePath = $appInfoDir . '/install-' . $this->architecture . '-' . $this->resource . '.json'; $content = $this->fileAccessHelper->file_get_contents($signaturePath); $signatureData = null; @@ -127,14 +134,10 @@ private function getSignatureData(): array { private function getHashesOfResource(): array { $signatureData = $this->getSignatureData(); - $expectedHashes = $signatureData['hashes']; - $filtered = array_filter($expectedHashes, function (string $key) { - return str_starts_with($key, $this->resource); - }, ARRAY_FILTER_USE_KEY); - if (!$filtered) { + if (count($signatureData['hashes']) === 0) { throw new EmptySignatureDataException('No signature files to ' . $this->resource); } - return $filtered; + return $signatureData; } private function getLibresignAppCertificate(): X509 { @@ -186,13 +189,14 @@ private function validateIfIssignedByLibresignAppCertificate(array $expectedHash } public function verify(string $architecture, $resource): array { + $this->signatureData = []; $this->architecture = $architecture; $this->resource = $resource; try { $expectedHashes = $this->getHashesOfResource(); // Compare the list of files which are not identical - $installPath = $this->getInstallPath() . '/' . $this->resource; + $installPath = $this->getInstallPath(); $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($installPath), $installPath); } catch (EmptySignatureDataException $th) { return [ @@ -208,13 +212,13 @@ public function verify(string $architecture, $resource): array { ]; } - $differencesA = array_diff($expectedHashes, $currentInstanceHashes); - $differencesB = array_diff($currentInstanceHashes, $expectedHashes); + $differencesA = array_diff($expectedHashes['hashes'], $currentInstanceHashes); + $differencesB = array_diff($currentInstanceHashes, $expectedHashes['hashes']); $differences = array_merge($differencesA, $differencesB); $differenceArray = []; foreach ($differences as $filename => $hash) { // Check if file should not exist in the new signature table - if (!array_key_exists($filename, $expectedHashes)) { + if (!array_key_exists($filename, $expectedHashes['hashes'])) { $differenceArray['EXTRA_FILE'][$filename]['expected'] = ''; $differenceArray['EXTRA_FILE'][$filename]['current'] = $hash; continue; @@ -222,14 +226,14 @@ public function verify(string $architecture, $resource): array { // Check if file is missing if (!array_key_exists($filename, $currentInstanceHashes)) { - $differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename]; + $differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes['hashes'][$filename]; $differenceArray['FILE_MISSING'][$filename]['current'] = ''; continue; } // Check if hash does mismatch - if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) { - $differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename]; + if ($expectedHashes['hashes'][$filename] !== $currentInstanceHashes[$filename]) { + $differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes['hashes'][$filename]; $differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename]; continue; } @@ -260,7 +264,7 @@ protected function getInternalPathOfFolder(ISimpleFolder $node): string { private function getInstallPath(): string { $folder = $this->getDataDir() . '/' . - $this->getInternalPathOfFolder($this->appData->getFolder($this->architecture)); + $this->getInternalPathOfFolder($this->appData->getFolder($this->architecture . '/' . $this->resource)); return $folder; } From 3864ed6f8e21a31594be1fb1b090e51e49aaee1c Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 14:55:39 -0300 Subject: [PATCH 07/11] chore: make possible set certificate to sign setup Signed-off-by: Vitor Mattos --- lib/Command/Developer/SignSetup.php | 4 +++- lib/Service/Install/SignSetupService.php | 27 ++++++++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/Command/Developer/SignSetup.php b/lib/Command/Developer/SignSetup.php index 85315762bc..726d0417cd 100644 --- a/lib/Command/Developer/SignSetup.php +++ b/lib/Command/Developer/SignSetup.php @@ -70,9 +70,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $x509->loadX509($keyBundle); $x509->setPrivateKey($rsa); try { + $this->signSetupService->setCertificate($x509); + $this->signSetupService->setPrivateKey($rsa); foreach ($this->signSetupService->getArchitectures() as $architecture) { foreach ($this->installService->getAvailableResources() as $resource) { - $this->signSetupService->writeAppSignature($x509, $rsa, $architecture, $resource); + $this->signSetupService->writeAppSignature($architecture, $resource); } } $output->writeln('Successfully signed'); diff --git a/lib/Service/Install/SignSetupService.php b/lib/Service/Install/SignSetupService.php index 3e498e02c4..5b2429c74b 100644 --- a/lib/Service/Install/SignSetupService.php +++ b/lib/Service/Install/SignSetupService.php @@ -34,6 +34,7 @@ class SignSetupService { private string $resource; private array $signatureData = []; private ?x509 $x509 = null; + private ?RSA $rsa= null; public function __construct( private EnvironmentHelper $environmentHelper, private FileAccessHelper $fileAccessHelper, @@ -52,6 +53,14 @@ public function getArchitectures(): array { return $appInfo['dependencies']['architecture']; } + public function setPrivateKey(RSA $privateKey): void { + $this->rsa = $privateKey; + } + + public function setCertificate(x509 $x509): void { + $this->x509 = $x509; + } + /** * Write the signature of the app in the specified folder * @@ -61,8 +70,6 @@ public function getArchitectures(): array { * @throws \Exception */ public function writeAppSignature( - X509 $certificate, - RSA $privateKey, string $architecture, string $resource, ) { @@ -72,7 +79,7 @@ public function writeAppSignature( try { $iterator = $this->getFolderIterator($this->getInstallPath()); $hashes = $this->generateHashes($iterator); - $signature = $this->createSignatureData($hashes, $certificate, $privateKey); + $signature = $this->createSignatureData($hashes); $this->fileAccessHelper->file_put_contents( $appInfoDir . '/install-' . $this->architecture . '-' . $this->resource . '.json', json_encode($signature, JSON_PRETTY_PRINT) @@ -340,21 +347,19 @@ private function isExcluded(string $filename): bool { * @param RSA $privateKey * @return array */ - private function createSignatureData(array $hashes, - X509 $certificate, - RSA $privateKey): array { + private function createSignatureData(array $hashes): array { ksort($hashes); - $privateKey->setSignatureMode(RSA::SIGNATURE_PSS); - $privateKey->setMGFHash('sha512'); + $this->rsa->setSignatureMode(RSA::SIGNATURE_PSS); + $this->rsa->setMGFHash('sha512'); // See https://tools.ietf.org/html/rfc3447#page-38 - $privateKey->setSaltLength(0); - $signature = $privateKey->sign(json_encode($hashes)); + $this->rsa->setSaltLength(0); + $signature = $this->rsa->sign(json_encode($hashes)); return [ 'hashes' => $hashes, 'signature' => base64_encode($signature), - 'certificate' => $certificate->saveX509($certificate->currentCert), + 'certificate' => $this->x509->saveX509($this->x509->currentCert), ]; } } From 64c9c35da0d7cdbfc83dd8a027f4fd6d377c878a Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 18:19:24 -0300 Subject: [PATCH 08/11] chore: make possible sign setup using local cert at dev env Signed-off-by: Vitor Mattos --- lib/Command/Base.php | 44 +-------- lib/Command/Install.php | 13 +++ lib/Service/Install/ConfigureCheckService.php | 1 + lib/Service/Install/InstallService.php | 37 +++++-- lib/Service/Install/SignSetupService.php | 98 +++++++++++++++++-- .../Service/Install/SignSetupServiceTest.php | 48 +++------ .../features/account/signature.feature | 4 +- .../integration/features/sign/request.feature | 6 +- tests/psalm-baseline.xml | 15 ++- 9 files changed, 165 insertions(+), 101 deletions(-) diff --git a/lib/Command/Base.php b/lib/Command/Base.php index 738cc8775a..82dfe8d758 100644 --- a/lib/Command/Base.php +++ b/lib/Command/Base.php @@ -13,50 +13,10 @@ use Psr\Log\LoggerInterface; class Base extends CommandBase { - /** @var InstallService */ - public $installService; - - /** @var LoggerInterface */ - protected $logger; - public function __construct( - InstallService $installService, - LoggerInterface $logger + public InstallService $installService, + protected LoggerInterface $logger ) { parent::__construct(); - $this->installService = $installService; - $this->logger = $logger; - } - - protected function installJava(): void { - $this->installService->installJava(); - } - - protected function uninstallJava(): void { - $this->installService->uninstallJava(); - } - - protected function installJSignPdf(): void { - $this->installService->installJSignPdf(); - } - - protected function uninstallJSignPdf(): void { - $this->installService->uninstallJSignPdf(); - } - - protected function installPdftk(): void { - $this->installService->installPdftk(); - } - - protected function uninstallPdftk(): void { - $this->installService->uninstallPdftk(); - } - - protected function installCfssl(): void { - $this->installService->installCfssl(); - } - - protected function uninstallCfssl(): void { - $this->installService->uninstallCfssl(); } } diff --git a/lib/Command/Install.php b/lib/Command/Install.php index 0162f90145..167cec9f98 100644 --- a/lib/Command/Install.php +++ b/lib/Command/Install.php @@ -10,6 +10,7 @@ use OCA\Libresign\Service\Install\InstallService; use OCP\AppFramework\Services\IAppConfig; +use OCP\IConfig; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -20,6 +21,7 @@ public function __construct( InstallService $installService, LoggerInterface $logger, private IAppConfig $appConfig, + private IConfig $config, ) { parent::__construct($installService, $logger); } @@ -64,6 +66,14 @@ protected function configure(): void { mode: InputOption::VALUE_REQUIRED, description: 'x86_64 or aarch64' ); + if ($this->config->getSystemValue('debug', false) === true) { + $this->addOption( + name: 'use-local-cert', + shortcut: null, + mode: InputOption::VALUE_NONE, + description: 'Use local cert' + ); + } } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -75,6 +85,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (in_array($architecture, ['x86_64', 'aarch64'])) { $this->installService->setArchitecture($architecture); } + if ($input->hasOption('use-local-cert') && $input->getOption('use-local-cert')) { + $this->installService->willUseLocalCert(); + } $all = $input->getOption('all'); if ($input->getOption('java') || $all) { $this->installService->installJava(); diff --git a/lib/Service/Install/ConfigureCheckService.php b/lib/Service/Install/ConfigureCheckService.php index 15772d8f5d..325288a80c 100644 --- a/lib/Service/Install/ConfigureCheckService.php +++ b/lib/Service/Install/ConfigureCheckService.php @@ -205,6 +205,7 @@ public function isDebugEnabled(): bool { } private function verify(string $resource): array { + $this->signSetupService->willUseLocalCert($this->isDebugEnabled()); $result = $this->signSetupService->verify($this->architecture, $resource); if (count($result) === 1 && $this->isDebugEnabled()) { if (isset($result['SIGNATURE_DATA_NOT_FOUND'])) { diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index dba60e9f82..17036eb438 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -61,6 +61,7 @@ class InstallService { 'cfssl' ]; private string $architecture; + private bool $willUseLocalCert = false; public function __construct( ICacheFactory $cacheFactory, @@ -342,9 +343,21 @@ public function setResource(string $resource): self { } public function isDownloadedFilesOk(): bool { + $this->signSetupService->willUseLocalCert($this->willUseLocalCert); return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0; } + public function willUseLocalCert(): void { + $this->willUseLocalCert = true; + } + + private function writeAppSignature(): void { + if (!$this->willUseLocalCert) { + return; + } + $this->signSetupService->writeAppSignature($this->architecture, $this->resource); + } + public function installJava(?bool $async = false): void { $this->setResource('java'); if ($async) { @@ -392,6 +405,7 @@ public function installJava(?bool $async = false): void { $extractor = new $class($comporessedInternalFileName); $extractor->extract($extractDir); unlink($comporessedInternalFileName); + $this->writeAppSignature(); } $this->appConfig->setAppValue('java_path', $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java'); @@ -455,6 +469,7 @@ public function installJSignPdf(?bool $async = false): void { $zip = new ZIP($extractDir . '/' . $compressedFileName); $zip->extract($extractDir); unlink($extractDir . '/' . $compressedFileName); + $this->writeAppSignature(); } $fullPath = $extractDir . '/jsignpdf-' . JSignPdfHandler::VERSION . '/JSignPdf.jar'; @@ -482,20 +497,25 @@ public function installPdftk(?bool $async = false): void { $this->runAsync(); return; } - $folder = $this->getEmptyFolder($this->resource); - try { - $file = $folder->getFile('pdftk.jar'); - } catch (\Throwable $th) { - $file = $folder->newFile('pdftk.jar'); - } - $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file); - if (!$this->isDownloadedFilesOk()) { + if ($this->isDownloadedFilesOk()) { + $folder = $this->getFolder($this->resource); + $file = $folder->getFile('pdftk.jar'); + $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file); + } else { + $folder = $this->getEmptyFolder($this->resource); + try { + $file = $folder->getFile('pdftk.jar'); + } catch (\Throwable $th) { + $file = $folder->newFile('pdftk.jar'); + } + $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file); $url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar'; /** WHEN UPDATE version: generate this hash handmade and update here */ $hash = '59a28bed53b428595d165d52988bf4cf'; $this->download($url, 'pdftk', $fullPath, $hash); + $this->writeAppSignature(); } $this->appConfig->setAppValue('pdftk_path', $fullPath); @@ -562,6 +582,7 @@ private function installCfsslByArchitecture(string $arcitecture): void { chmod($fullPath, 0700); } + $this->writeAppSignature(); } $cfsslBinPath = $this->getDataDir() . '/' . diff --git a/lib/Service/Install/SignSetupService.php b/lib/Service/Install/SignSetupService.php index 5b2429c74b..795dbd9e23 100644 --- a/lib/Service/Install/SignSetupService.php +++ b/lib/Service/Install/SignSetupService.php @@ -33,8 +33,9 @@ class SignSetupService { private string $architecture; private string $resource; private array $signatureData = []; - private ?x509 $x509 = null; - private ?RSA $rsa= null; + private bool $willUseLocalCert = false; + private ?X509 $x509 = null; + private ?RSA $rsa = null; public function __construct( private EnvironmentHelper $environmentHelper, private FileAccessHelper $fileAccessHelper, @@ -61,6 +62,38 @@ public function setCertificate(x509 $x509): void { $this->x509 = $x509; } + public function willUseLocalCert(bool $willUseLocalCert): void { + $this->willUseLocalCert = $willUseLocalCert; + } + + private function getPrivateKey(): RSA { + if (!$this->rsa instanceof RSA) { + $oi = __DIR__ . '/../../../build/tools/certificates/local/libresign.key'; + if (file_exists(__DIR__ . '/../../../build/tools/certificates/local/libresign.key')) { + $privateKey = file_get_contents(__DIR__ . '/../../../build/tools/certificates/local/libresign.key'); + $this->rsa = new RSA(); + $this->rsa->loadKey($privateKey); + } else { + $this->getDevelopCert(); + } + } + return $this->rsa; + } + + private function getCertificate(): X509 { + if (!$this->x509 instanceof x509) { + if (file_exists(__DIR__ . '/../../../build/tools/certificates/local/libresign.crt')) { + $x509 = file_get_contents(__DIR__ . '/../../../build/tools/certificates/local/libresign.crt'); + $this->x509 = new X509(); + $this->x509->loadX509($x509); + $this->x509->setPrivateKey($this->getPrivateKey()); + } else { + $this->getDevelopCert(); + } + } + return $this->x509; + } + /** * Write the signature of the app in the specified folder * @@ -155,8 +188,8 @@ private function getLibresignAppCertificate(): X509 { $certificate = $signatureData['certificate']; // Check if certificate is signed by Nextcloud Root Authority + $rootCertificatePublicKey = $this->getRootCertificatePublicKey(); $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) { @@ -350,16 +383,65 @@ private function isExcluded(string $filename): bool { private function createSignatureData(array $hashes): array { ksort($hashes); - $this->rsa->setSignatureMode(RSA::SIGNATURE_PSS); - $this->rsa->setMGFHash('sha512'); + $this->getPrivateKey()->setSignatureMode(RSA::SIGNATURE_PSS); + $this->getPrivateKey()->setMGFHash('sha512'); // See https://tools.ietf.org/html/rfc3447#page-38 - $this->rsa->setSaltLength(0); - $signature = $this->rsa->sign(json_encode($hashes)); + $this->getPrivateKey()->setSaltLength(0); + $signature = $this->getPrivateKey()->sign(json_encode($hashes)); return [ 'hashes' => $hashes, 'signature' => base64_encode($signature), - 'certificate' => $this->x509->saveX509($this->x509->currentCert), + 'certificate' => $this->getCertificate()->saveX509($this->getCertificate()->currentCert), + ]; + } + + private function getRootCertificatePublicKey(): string { + if ($this->willUseLocalCert) { + $localCert = __DIR__ . '/../../../build/tools/certificates/local/root.crt'; + if (file_exists($localCert)) { + return file_get_contents($localCert); + } + } + return $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt'); + } + + public function getDevelopCert(): array { + $privateKey = openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + + $csrNames = ['commonName' => 'libresign']; + + $csr = openssl_csr_new($csrNames, $privateKey, ['digest_alg' => 'sha256']); + $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365, ['digest_alg' => 'sha256']); + + openssl_x509_export($x509, $rootCertificate); + openssl_pkey_export($privateKey, $privateKeyCert); + + $this->rsa = new RSA(); + $this->rsa->loadKey($privateKeyCert); + $this->x509 = new X509(); + $this->x509->loadX509($rootCertificate); + $this->x509->setPrivateKey($this->rsa); + + $rootCertPath = __DIR__ . '/../../../build/tools/certificates/local/'; + if (!is_dir($rootCertPath)) { + mkdir($rootCertPath, 0777, true); + } + file_put_contents($rootCertPath . '/root.crt', $rootCertificate); + file_put_contents($rootCertPath . '/libresign.crt', $rootCertificate); + file_put_contents($rootCertPath . '/libresign.key', $privateKeyCert); + + $privateKeyInstance = openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + return [ + 'rootCertificate' => $rootCertificate, + 'privateKeyInstance' => $privateKeyInstance, + 'privateKeyCert' => $privateKeyCert, ]; } } diff --git a/tests/Unit/Service/Install/SignSetupServiceTest.php b/tests/Unit/Service/Install/SignSetupServiceTest.php index 3895518bca..cdb6956fd6 100644 --- a/tests/Unit/Service/Install/SignSetupServiceTest.php +++ b/tests/Unit/Service/Install/SignSetupServiceTest.php @@ -50,31 +50,6 @@ private function getInstance(array $methods = []) { ->getMock(); } - public function getDevelopCert(): array { - $privateKey = openssl_pkey_new([ - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]); - - $csrNames = ['commonName' => 'libresign']; - - $csr = openssl_csr_new($csrNames, $privateKey, ['digest_alg' => 'sha256']); - $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365, ['digest_alg' => 'sha256']); - - openssl_x509_export($x509, $rootCertificate); - openssl_pkey_export($privateKey, $privateKeyCert); - - $privateKeyInstance = openssl_pkey_new([ - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]); - return [ - 'rootCertificate' => $rootCertificate, - 'privateKeyInstance' => $privateKeyInstance, - 'privateKeyCert' => $privateKeyCert, - ]; - } - /** * @dataProvider dataGetArchitectures */ @@ -100,7 +75,7 @@ public static function dataGetArchitectures(): array { ]; } - private function writeAppSignature(string $architecture): SignSetupService { + private function writeAppSignature(string $architecture, $resource): SignSetupService { $this->config->method('getSystemValue') ->willReturn(vfsStream::url('home/data')); @@ -121,7 +96,7 @@ private function writeAppSignature(string $architecture): SignSetupService { $this->appManager->method('getAppInfo') ->willReturn(['dependencies' => ['architecture' => [$architecture]]]); - $certificate = $this->getDevelopCert(); + $certificate = $signSetupService->getDevelopCert(); $rsa = new RSA(); $rsa->loadKey($certificate['privateKeyInstance']); $rsa->loadKey($certificate['privateKeyCert']); @@ -147,13 +122,13 @@ private function writeAppSignature(string $architecture): SignSetupService { ]; $root = vfsStream::setup('home', null, $structure); - $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'); + $signSetupService->writeAppSignature($architecture, $resource); + $this->assertFileExists('vfs://home/appinfo/install-' . $architecture . '-' . $resource . '.json'); + $json = file_get_contents('vfs://home/appinfo/install-' . $architecture . '-' . $resource . '.json'); $signatureContent = json_decode($json, true); $this->assertArrayHasKey('hashes', $signatureContent); $this->assertCount(2, $signatureContent['hashes']); - $expected = hash('sha512', $structure['data']['libresign']['java']['fakeFile01']); + $expected = hash('sha512', $structure['data']['libresign'][$resource]['fakeFile01']); $actual = $signatureContent['hashes']['java/fakeFile01']; $this->assertEquals($expected, $actual); return $signSetupService; @@ -162,23 +137,22 @@ private function writeAppSignature(string $architecture): SignSetupService { /** * @dataProvider dataWriteAppSignature */ - public function testWriteAppSignature(string $architecture): void { - $signSetupService = $this->writeAppSignature($architecture); - $architecture = 'x86_64'; - $resource = 'java'; + public function testWriteAppSignature(string $architecture, $resource): void { + $signSetupService = $this->writeAppSignature($architecture, $resource); $actual = $signSetupService->verify($architecture, $resource); $this->assertCount(0, $actual); } public static function dataWriteAppSignature(): array { return [ - ['x86_64', 'aarch64'], + ['x86_64', 'java'], + ['aarch64', 'java'], ]; } public function testVerify(): void { $architecture = 'x86_64'; - $signSetupService = $this->writeAppSignature($architecture); + $signSetupService = $this->writeAppSignature($architecture, 'java'); 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'); diff --git a/tests/integration/features/account/signature.feature b/tests/integration/features/account/signature.feature index 14dbd2a7ea..64da00193f 100644 --- a/tests/integration/features/account/signature.feature +++ b/tests/integration/features/account/signature.feature @@ -20,7 +20,7 @@ Feature: account/signature Scenario: Create root certificate with CFSSL engine using API Given as user "admin" And run the command "config:app:set libresign certificate_engine --value cfssl" with result code 0 - And run the command "libresign:install --cfssl" with result code 0 + And run the command "libresign:install --use-local-cert --cfssl" with result code 0 And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/cfssl" | rootCert | {"commonName":"Common Name","names":{"C":{"id":"C","value":"BR"},"ST":{"id":"ST","value":"State of Company"},"L":{"id":"L","value":"City name"},"O":{"id":"O","value":"Organization"},"OU":{"id":"OU","value":"Organizational Unit"}}} | And the response should have a status code 200 @@ -42,7 +42,7 @@ Feature: account/signature And set the email of user "signer1" to "signer@domain.test" And as user "signer1" And run the command "config:app:set libresign certificate_engine --value cfssl" with result code 0 - And run the command "libresign:install --cfssl" with result code 0 + And run the command "libresign:install --use-local-cert --cfssl" with result code 0 And run the command "libresign:configure:cfssl --cn=Common\ Name --c=BR --o=Organization --st=State\ of\ Company --l=City\ Name --ou=Organization\ Unit" with result code 0 And sending "post" to ocs "/apps/libresign/api/v1/account/signature" | signPassword | password | diff --git a/tests/integration/features/sign/request.feature b/tests/integration/features/sign/request.feature index 37e57eb3b3..39782c307e 100644 --- a/tests/integration/features/sign/request.feature +++ b/tests/integration/features/sign/request.feature @@ -317,9 +317,9 @@ Feature: request-signature Scenario: Sign file Given as user "admin" And user "signer1" exists - And run the command "libresign:install --java" with result code 0 - And run the command "libresign:install --jsignpdf" with result code 0 - And run the command "libresign:install --pdftk" with result code 0 + And run the command "libresign:install --use-local-cert --java" with result code 0 + And run the command "libresign:install --use-local-cert --jsignpdf" with result code 0 + And run the command "libresign:install --use-local-cert --pdftk" with result code 0 And run the command "libresign:configure:openssl --cn=Common\ Name --c=BR --o=Organization --st=State\ of\ Company --l=City\ Name --ou=Organization\ Unit" with result code 0 And run the command "config:app:set libresign add_footer --value=1" with result code 0 And run the command "config:app:set libresign write_qrcode_on_footer --value=1" with result code 0 diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index ce5b4bf9d1..56ff9cc416 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -128,16 +128,29 @@ + getCertificate()]]> + getCertificate()->currentCert]]> + getPrivateKey()]]> + getPrivateKey()]]> + getPrivateKey()]]> + getPrivateKey()]]> currentCert]]> - + + + + + + + + From 9c207a151a7c39dababda25090df87114b0f61a9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 18:24:25 -0300 Subject: [PATCH 09/11] fix: psalm Signed-off-by: Vitor Mattos --- tests/psalm-baseline.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 56ff9cc416..82a687f438 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -148,7 +148,6 @@ - From 9e102e94ce9a7f54a992158742dfa1eb611c93ec Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 18:30:56 -0300 Subject: [PATCH 10/11] chore: remove unused code Signed-off-by: Vitor Mattos --- lib/Command/Developer/SignSetup.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/Command/Developer/SignSetup.php b/lib/Command/Developer/SignSetup.php index 726d0417cd..34d80a362a 100644 --- a/lib/Command/Developer/SignSetup.php +++ b/lib/Command/Developer/SignSetup.php @@ -37,14 +37,12 @@ protected function configure(): void { $this ->setName('libresign:developer:sign-setup') ->setDescription('Sign the current setup') - ->addOption('develop', null, InputOption::VALUE_NONE, 'If develop mode enabled, do not will be necessary to use privateKey and certificate.') ->addOption('privateKey', null, InputOption::VALUE_REQUIRED, 'Path to private key to use for signing') ->addOption('certificate', null, InputOption::VALUE_REQUIRED, 'Path to certificate to use for signing') ; } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->handleDevelopMode($input); $privateKeyPath = $input->getOption('privateKey'); $keyBundlePath = $input->getOption('certificate'); if (is_null($privateKeyPath) || is_null($keyBundlePath)) { @@ -84,20 +82,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int } return 0; } - - private function handleDevelopMode(InputInterface $input): void { - $develop = $input->getOption('develop'); - if (!$develop) { - return; - } - if (file_exists(__DIR__ . '/../../../build/tools/certificates/libresign.crt')) { - if (!$input->getOption('privateKey')) { - $input->setOption('certificate', __DIR__ . '/../../../build/tools/certificates/libresign.crt'); - } - if (!$input->getOption('certificate')) { - $input->setOption('privateKey', __DIR__ . '/../../../build/tools/certificates/libresign.key'); - } - return; - } - } } From 4358759b6c5ad854878a4ca84d49476ac2684ed0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 17 Jun 2024 18:43:05 -0300 Subject: [PATCH 11/11] fix: default cfssl path Signed-off-by: Vitor Mattos --- lib/Service/Install/InstallService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index 17036eb438..bb75b39779 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -586,8 +586,7 @@ private function installCfsslByArchitecture(string $arcitecture): void { } $cfsslBinPath = $this->getDataDir() . '/' . - $this->getInternalPathOfFolder($folder) . '/' . - $downloads[0]['destination']; + $this->getInternalPathOfFolder($folder) . '/cfssl'; $this->appConfig->setAppValue('cfssl_bin', $cfsslBinPath); }