From a45d455cf716207855f56283d09921ce51b994b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maia?= Date: Fri, 4 Sep 2026 22:49:39 -0300 Subject: [PATCH 1/8] build(deps): update 3rdparty to jsignpdf-php 3.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Points the submodule at LibreSign/3rdparty#96, which bumps jsignpdf/jsignpdf-php from 1.3.1 to 3.0.0 for JSignPdf 3.1.0. The pointer will move to the merge commit once that pull request lands. Signed-off-by: André Maia Assisted-by: Claude Code:claude-fable-5-1 --- 3rdparty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty b/3rdparty index cddfc39be9..63d763a37f 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit cddfc39be9e5841421ffb954f919afc2ff6a9809 +Subproject commit 63d763a37f2a1f26a312a24e0286c4281eb3cc86 From e5a34864165453109b02e66b9446ec9a81879784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maia?= Date: Fri, 4 Sep 2026 22:49:39 -0300 Subject: [PATCH 2/8] feat(install): install JSignPdf 3.1.0 from the minimal package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSignPdf 3.1 dropped the single JSignPdf.jar. The minimal package, meant for headless use, ships a lib/ directory that the wrapper starts through the classpath, so the configured path becomes the extracted directory, stored in the new jsignpdf_path key. The legacy jsignpdf_jar_path key is removed on install and on uninstall, and the archive is verified against the SHA256SUMS file published with the release instead of a hard-coded md5. The release metadata lives in JSignPdfRelease so it can be unit tested; SignSetupService and the setup check read the new key. Signed-off-by: André Maia Assisted-by: Claude Code:claude-fable-5-1 --- lib/Service/Install/InstallService.php | 29 ++++---- lib/Service/Install/JSignPdfRelease.php | 49 +++++++++++++ lib/Service/Install/SignSetupService.php | 4 +- lib/SetupCheck/JSignPdfSetupCheck.php | 12 ++-- .../Service/Install/JSignPdfReleaseTest.php | 58 +++++++++++++++ .../Service/Install/SignSetupServiceTest.php | 2 +- .../SetupCheck/JSignPdfSetupCheckTest.php | 72 +++++++++++++------ 7 files changed, 181 insertions(+), 45 deletions(-) create mode 100644 lib/Service/Install/JSignPdfRelease.php create mode 100644 tests/php/Unit/Service/Install/JSignPdfReleaseTest.php diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index f24ed55350..e1f5623dd4 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -50,8 +50,7 @@ class InstallService { private const string JAVA_URL_PATH_NAME = '21.0.8+9'; public const PDFTK_VERSION = '3.3.3'; /** @todo When update, verify the hash **/ private const string PDFTK_HASH = '59a28bed53b428595d165d52988bf4cf'; - public const JSIGNPDF_VERSION = '2.3.0'; /** @todo When update, verify the hash **/ - private const string JSIGNPDF_HASH = 'd239658ea50a39eb35169d8392feaffb'; + public const JSIGNPDF_VERSION = JSignPdfRelease::VERSION; public const CFSSL_VERSION = '1.6.5'; private const string PROCESS_SOURCE = 'install'; @@ -477,36 +476,34 @@ public function installJSignPdf(?bool $async = false): void { if ($this->isDownloadedFilesOk()) { // The binaries files could exists but not saved at database - $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); if (!$fullPath) { $folder = $this->getFolder($this->resource); - $extractDir = $this->getInternalPathOfFolder($folder); - $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar'; - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath); + $fullPath = JSignPdfRelease::installPath($this->getInternalPathOfFolder($folder)); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', $fullPath); } $this->saveJsignPdfHome(); - if (str_contains($fullPath, InstallService::JSIGNPDF_VERSION)) { + if (str_contains($fullPath, InstallService::JSIGNPDF_VERSION) && is_dir($fullPath)) { return; } } $folder = $this->getFolder($this->resource); - $compressedFileName = 'jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip'; + $compressedFileName = JSignPdfRelease::archiveName(); try { $compressedFile = $folder->getFile($compressedFileName); } catch (\Throwable) { $compressedFile = $folder->newFile($compressedFileName); } $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile); - $url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', InstallService::JSIGNPDF_VERSION) . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip'; - - $this->download($url, 'JSignPdf', $compressedInternalFileName, self::JSIGNPDF_HASH); + $hash = $this->getHash($compressedFileName, JSignPdfRelease::checksumUrl()); + $this->download(JSignPdfRelease::downloadUrl(), 'JSignPdf', $compressedInternalFileName, $hash, 'sha256'); $extractDir = $this->getInternalPathOfFolder($folder); $zip = new ZIP($extractDir . '/' . $compressedFileName); $zip->extract($extractDir); unlink($extractDir . '/' . $compressedFileName); - $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar'; - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', JSignPdfRelease::installPath($extractDir)); + $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path'); $this->saveJsignPdfHome(); $this->writeAppSignature(); @@ -536,8 +533,9 @@ private function saveJsignPdfHome(): void { } public function uninstallJSignPdf(): void { - $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); - if (!$jsignpdJarPath) { + $jsignpdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path') + ?: $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + if (!$jsignpdfPath) { return; } $this->setResource('jsignpdf'); @@ -546,6 +544,7 @@ public function uninstallJSignPdf(): void { $folder->delete(); } catch (NotFoundException) { } + $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_path'); $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path'); $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_home'); } diff --git a/lib/Service/Install/JSignPdfRelease.php b/lib/Service/Install/JSignPdfRelease.php new file mode 100644 index 0000000000..4929acd5dd --- /dev/null +++ b/lib/Service/Install/JSignPdfRelease.php @@ -0,0 +1,49 @@ +appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + $path = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); if (!$path) { // fallback try { @@ -212,7 +212,7 @@ public function getInstallPath(): string { throw new InvalidSignatureException('JSignPdf path not found at app config.'); } } - $installPath = substr($path, 0, strrpos($path, '/', -strlen('_/JSignPdf.jar'))); + $installPath = dirname($path); break; case 'pdftk': $path = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path'); diff --git a/lib/SetupCheck/JSignPdfSetupCheck.php b/lib/SetupCheck/JSignPdfSetupCheck.php index 22dee5b717..873a51eb47 100644 --- a/lib/SetupCheck/JSignPdfSetupCheck.php +++ b/lib/SetupCheck/JSignPdfSetupCheck.php @@ -71,9 +71,9 @@ public function getCategory(): string { #[\Override] public function run(): SetupResult { $debugEnabled = $this->systemConfig->getSystemValueBool('debug', false); - $jsignpdfJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + $jsignpdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); - if (!$jsignpdfJarPath) { + if (!$jsignpdfPath) { return SetupResult::error( // TRANSLATORS Warning shown in Nextcloud administration overview when the optional JSignPdf signing backend is not found. $this->l10n->t('JSignPdf not found'), @@ -88,12 +88,12 @@ public function run(): SetupResult { return SetupResult::error($errorMsg, $tip); } - if (!file_exists($jsignpdfJarPath)) { + if (!is_dir($jsignpdfPath)) { return SetupResult::error( // TRANSLATORS JSignPdf is an optional external signing backend used by LibreSign. // LibreSign also supports other signing methods, including its native PHP signer. // %s is the configured JSignPdf path that could not be found. - $this->l10n->t('JSignPdf file not found: %s', [$jsignpdfJarPath]), + $this->l10n->t('JSignPdf path not found: %s', [$jsignpdfPath]), // TRANSLATORS Command to run into terminal using Nextcloud occ to configure LibreSign using CLI when the sysadmin want to do this by CLI. $this->l10n->t('Run %s', ['occ libresign:install --jsignpdf']) ); @@ -149,8 +149,8 @@ public function run(): SetupResult { // TRANSLATORS JSignPdf is an optional external signing backend. %s is the detected JSignPdf version. $this->l10n->t('JSignPdf version: %s', [$currentVersion]), - // TRANSLATORS JSignPdf is an optional external signing backend. %s is the configured or detected path to the JSignPdf executable/JAR file. - $this->l10n->t('JSignPdf path: %s', [$jsignpdfJarPath]), + // TRANSLATORS JSignPdf is an optional external signing backend. %s is the configured or detected directory where JSignPdf is installed. + $this->l10n->t('JSignPdf path: %s', [$jsignpdfPath]), ]; return SetupResult::success(implode("\n", $messages)); diff --git a/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php b/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php new file mode 100644 index 0000000000..24d63fe394 --- /dev/null +++ b/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php @@ -0,0 +1,58 @@ +assertSame('3.1.0', JSignPdfRelease::VERSION); + $this->assertSame('jsignpdf-3.1.0-minimal.zip', JSignPdfRelease::archiveName()); + } + + public function testDownloadUrlPointsToTheArchiveOfTheReleaseTag(): void { + $this->assertSame( + self::RELEASE_URL . 'jsignpdf-3.1.0-minimal.zip', + JSignPdfRelease::downloadUrl(), + ); + } + + public function testChecksumUrlPointsToTheSha256SumsOfTheSameRelease(): void { + $this->assertSame( + self::RELEASE_URL . 'jsignpdf-3.1.0-SHA256SUMS.txt', + JSignPdfRelease::checksumUrl(), + ); + } + + #[DataProvider('providerInstallPath')] + public function testInstallPathIsTheVersionedDirectoryInsideTheExtractDir(string $extractDir, string $expected): void { + $this->assertSame($expected, JSignPdfRelease::installPath($extractDir)); + } + + public static function providerInstallPath(): array { + return [ + 'appdata folder' => [ + '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf', + '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf/jsignpdf-3.1.0', + ], + 'path with spaces' => [ + '/opt/libre sign/jsignpdf', + '/opt/libre sign/jsignpdf/jsignpdf-3.1.0', + ], + 'relative path' => [ + 'jsignpdf', + 'jsignpdf/jsignpdf-3.1.0', + ], + ]; + } +} diff --git a/tests/php/Unit/Service/Install/SignSetupServiceTest.php b/tests/php/Unit/Service/Install/SignSetupServiceTest.php index f13dd6b76f..37d33a1567 100644 --- a/tests/php/Unit/Service/Install/SignSetupServiceTest.php +++ b/tests/php/Unit/Service/Install/SignSetupServiceTest.php @@ -199,7 +199,7 @@ public function testVerify(): void { #[DataProvider('dataGetInstallPath')] public function testGetInstallPath(string $architecture, string $resource, string $distro, string $expected): void { $this->appConfig->setValueString(Application::APP_ID, 'java_path', 'vfs://home/data/appdata_1/libresign/x86_64/linux/java/jdk-21.0.2+13-jre/bin/java'); - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', 'vfs://home/data/appdata_1/libresign/x86_64/jsignpdf/jsignpdf-2.2.2/JSignPdf.jar'); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', 'vfs://home/data/appdata_1/libresign/x86_64/jsignpdf/jsignpdf-3.1.0'); $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', 'vfs://home/data/appdata_1/libresign/x86_64/pdftk/pdftk.jar'); $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', 'vfs://home/data/appdata_1/libresign/x86_64/cfssl/cfssl'); $actual = $this->getInstance() diff --git a/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php b/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php index 3aa551278a..94a73f3166 100644 --- a/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php +++ b/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php @@ -14,6 +14,12 @@ function file_exists(string $filename): bool { } } +if (!function_exists('OCA\Libresign\SetupCheck\is_dir')) { + function is_dir(string $filename): bool { + return \OCA\Libresign\Tests\Mock\FileSystemMock::fileExists($filename); + } +} + namespace OCA\Libresign\Tests\Unit\SetupCheck; use OCA\Libresign\Handler\SignEngine\JSignPdfHandler; @@ -95,7 +101,7 @@ public function testGetCategory(): void { public function testRunNoPathConfigured(): void { $this->mockTranslation(); $this->appConfig->method('getValueString') - ->with('libresign', 'jsignpdf_jar_path') + ->with('libresign', 'jsignpdf_path') ->willReturn(''); $result = $this->check->run(); @@ -134,9 +140,9 @@ public function testRunVerifyFails(): void { public function testRunBinaryNotFound(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); @@ -144,26 +150,26 @@ public function testRunBinaryNotFound(): void { $this->signSetupService->method('verify') ->willReturn([]); - FileSystemMock::$files[$jarPath] = false; + FileSystemMock::$files[$jsignPdfPath] = false; $result = $this->check->run(); $this->assertInstanceOf(SetupResult::class, $result); $this->assertSame('error', $result->getSeverity()); - $this->assertStringContainsString('JSignPdf file not found', $result->getDescription()); + $this->assertStringContainsString('JSignPdf path not found', $result->getDescription()); } public function testRunJavaNotFound(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify') ->willReturn([]); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; $this->javaHelper->method('getJavaPath') ->willReturn(''); @@ -175,6 +181,29 @@ public function testRunJavaNotFound(): void { $this->assertStringContainsString('Necessary Java to run JSignPdf', $result->getDescription()); } + public function testRunJavaBinaryMissing(): void { + $this->mockTranslation(); + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $this->appConfig->method('getValueString') + ->willReturn($jsignPdfPath); + $this->systemConfig->method('getSystemValueBool') + ->willReturn(false); + $this->signSetupService->method('verify') + ->willReturn([]); + + FileSystemMock::$files[$jsignPdfPath] = true; + FileSystemMock::$files['/opt/java/bin/java'] = false; + + $this->javaHelper->method('getJavaPath') + ->willReturn('/opt/java/bin/java'); + + $result = $this->check->run(); + + $this->assertInstanceOf(SetupResult::class, $result); + $this->assertSame('error', $result->getSeverity()); + $this->assertStringContainsString('Necessary Java to run JSignPdf', $result->getDescription()); + } + private function createJSignParamMock() { return $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\Sign\JSignParam::class) ->disableOriginalConstructor() @@ -183,14 +212,14 @@ private function createJSignParamMock() { public function testRunVersionEmpty(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -200,6 +229,7 @@ public function testRunVersionEmpty(): void { $jsignPdfMock->method('getVersion')->willReturn(''); $jsignParamMock = $this->createJSignParamMock(); + $jsignPdfMock->expects($this->once())->method('setParam')->with($jsignParamMock); $this->jSignPdfHandler->method('getJSignPdf')->willReturn($jsignPdfMock); $this->jSignPdfHandler->method('getJSignParam')->willReturn($jsignParamMock); @@ -213,14 +243,14 @@ public function testRunVersionEmpty(): void { public function testRunVersionTooLow(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -243,14 +273,14 @@ public function testRunVersionTooLow(): void { public function testRunVersionTooHigh(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -273,14 +303,14 @@ public function testRunVersionTooHigh(): void { public function testRunSuccess(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -299,6 +329,6 @@ public function testRunSuccess(): void { $this->assertInstanceOf(SetupResult::class, $result); $this->assertSame('success', $result->getSeverity()); $this->assertStringContainsString('JSignPdf version: ' . InstallService::JSIGNPDF_VERSION, $result->getDescription()); - $this->assertStringContainsString('JSignPdf path: ' . $jarPath, $result->getDescription()); + $this->assertStringContainsString('JSignPdf path: ' . $jsignPdfPath, $result->getDescription()); } } From eab0c364a0b4d598d6faa6207e61cd7a7cdd046c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maia?= Date: Fri, 4 Sep 2026 22:49:40 -0300 Subject: [PATCH 3/8] refactor(jsignpdf): pass structured parameters to the jsignpdf-php 3 wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jsignpdf-php 3 escapes every argument itself and takes the options as a list, so the handler stops building shell syntax: the parameters are arrays, one cloned JSignParam per visible element, the signature text is passed as is instead of quoted, the JVM option and the JSIGNPDF_HOME variable go through their own setters, and the TSA password goes through setTsaPassword(), which the wrapper writes to the stdin of JSignPdf instead of the command line. The wrapper also renamed the jar path to a directory path and reports failures through the exit code. The tests assert the parameters that reach the wrapper, captured from setParam(), with structured providers, a case with shell special characters in the signature text and a TSA provider that checks the password never reaches the command line. Signed-off-by: André Maia Assisted-by: Claude Code:claude-fable-5-1 --- lib/Handler/SignEngine/JSignPdfHandler.php | 155 ++++--- .../SignEngine/JSignPdfHandlerTest.php | 391 +++++++++++++++--- 2 files changed, 396 insertions(+), 150 deletions(-) diff --git a/lib/Handler/SignEngine/JSignPdfHandler.php b/lib/Handler/SignEngine/JSignPdfHandler.php index 2dc7ddc143..e197ee1bcb 100644 --- a/lib/Handler/SignEngine/JSignPdfHandler.php +++ b/lib/Handler/SignEngine/JSignPdfHandler.php @@ -15,7 +15,6 @@ use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Helper\JavaHelper; use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService; -use OCA\Libresign\Service\Install\InstallService; use OCA\Libresign\Service\Policy\PolicyService; use OCA\Libresign\Service\Policy\Provider\SignatureHashAlgorithm\SignatureHashAlgorithmPolicy; use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicyValue; @@ -82,34 +81,29 @@ public function getJSignParam(): JSignParam { if (!is_writable($tempPath)) { throw new \Exception('The path ' . $tempPath . ' is not writtable. Fix this or change the LibreSign app setting jsignpdf_temp_path to a writtable path'); } - $jSignPdfJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path', '/opt/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar'); - if (!file_exists($jSignPdfJarPath)) { - throw new \Exception('Invalid JSignPdf jar path. Run occ libresign:install --jsignpdf'); + $jSignPdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); + if (!is_dir($jSignPdfPath)) { + throw new \Exception('Invalid JSignPdf path. Run occ libresign:install --jsignpdf'); } + $home = $this->getHome(); $this->jSignParam = (new JSignParam()) ->setTempPath($tempPath) ->setIsUseJavaInstalled(empty($javaPath)) ->setJavaDownloadUrl('') ->setJSignPdfDownloadUrl('') - ->setjSignPdfJarPath($jSignPdfJarPath); + ->setJSignPdfPath($jSignPdfPath) + ->setJavaOptions(['-Duser.home=' . $home]) + ->setEnvironmentVariables(['JSIGNPDF_HOME' => $home]); if (!empty($javaPath)) { if (!file_exists($javaPath)) { throw new \Exception('Invalid Java binary. Run occ libresign:install --java'); } - $this->jSignParam->setJavaPath( - $this->getEnvironments() - . $javaPath - . ' -Duser.home=' . escapeshellarg($this->getHome()) . ' ' - ); + $this->jSignParam->setJavaPath($javaPath); } } return $this->jSignParam; } - private function getEnvironments(): string { - return 'JSIGNPDF_HOME=' . escapeshellarg($this->getHome()) . ' '; - } - /** * It's a workaround to create the folder structure that JSignPdf needs. Without * this, the JSignPdf will return the follow message to all commands: @@ -264,36 +258,28 @@ public function getSignedContent(): string { $normalizedPdf = $this->normalizePdfVersion($this->getInputFile()->getContent()); $hashAlgorithm = $this->getHashAlgorithm($normalizedPdf); $param = $this->getJSignParam(); - - $tsaParams = $this->listParamsToString($this->getTsaParameters()); - - $visibleElements = $this->getVisibleElements(); - $certParams = ''; - $certificationLevel = $this->getCertificationLevel(); - if ($certificationLevel !== null && !$visibleElements && !$this->hasExistingSignatures($normalizedPdf)) { - $certParams = ' -cl ' . $certificationLevel; - } - - $param->setJSignParameters( - $param->getJSignParameters() - . $certParams - . $tsaParams - ); $param->setCertificate($this->getCertificate()) ->setPdf($normalizedPdf) ->setPassword($this->getPassword()); + $parameters = []; + $certificationLevel = $this->getCertificationLevel(); + if ($certificationLevel !== null && !$this->getVisibleElements() && !$this->hasExistingSignatures($normalizedPdf)) { + $parameters['-cl'] = $certificationLevel; + } + $parameters += $this->getTsaParameters(); + $param->addJSignParameters($parameters); + $tsaPassword = $this->getTsaPassword(); + if ($tsaPassword !== '') { + $param->setTsaPassword($tsaPassword); + } + $signed = $this->signUsingVisibleElements($normalizedPdf, $hashAlgorithm); if ($signed) { return $signed; } - $param->setJSignParameters( - $param->getJSignParameters() - . $this->listParamsToString([ - '--hash-algorithm' => $hashAlgorithm, - ]) - ); + $param->addJSignParameters(['--hash-algorithm' => $hashAlgorithm]); $jSignPdf = $this->getJSignPdf(); $jSignPdf->setParam($param); return $this->signWrapper($jSignPdf); @@ -313,7 +299,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg ]; // When l2-text is empty, add hash-algorithm at the beginning - if ($params['--l2-text'] === '""') { + if ($params['--l2-text'] === '') { $params = [ '--hash-algorithm' => $hashAlgorithm, '--l2-text' => $params['--l2-text'], @@ -322,7 +308,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } $fontSize = $this->parseSignatureText()['templateFontSize']; - if ($fontSize === SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE || !$fontSize || $params['--l2-text'] === '""') { + if ($fontSize === SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE || !$fontSize || $params['--l2-text'] === '') { $fontSize = 0; } @@ -335,11 +321,9 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg $certificationLevel = $this->getCertificationLevel(); $applyCertification = $certificationLevel !== null && !$this->hasExistingSignatures($normalizedPdf); - $certParams = $applyCertification ? ' -cl ' . $certificationLevel : ''; $elementIndex = 0; - $param = $this->getJSignParam(); - $originalParam = clone $param; + $originalParam = $this->getJSignParam(); foreach ($visibleElements as $element) { $elementIndex++; @@ -374,7 +358,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } elseif ($signatureImagePath) { $params['--bg-path'] = $signatureImagePath; } - } elseif ($params['--l2-text'] === '""') { + } elseif ($params['--l2-text'] === '') { if ($backgroundPathForElement && $signatureImagePath) { $params['--bg-path'] = $this->mergeBackgroundWithSignature( $backgroundPathForElement, @@ -409,16 +393,15 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } // Only add hash-algorithm at the end if l2-text is not empty - if ($params['--l2-text'] !== '""') { + if ($params['--l2-text'] !== '') { $params['--hash-algorithm'] = $hashAlgorithm; } - $elementCertParams = ($applyCertification && $elementIndex === 1) ? $certParams : ''; - $param->setJSignParameters( - $originalParam->getJSignParameters() - . $elementCertParams - . $this->listParamsToString($params) - ); + $param = clone $originalParam; + if ($applyCertification && $elementIndex === 1) { + $param->addJSignParameters(['-cl' => $certificationLevel]); + } + $param->addJSignParameters($this->toJSignParameters($params)); $param->setPdf($normalizedPdf); $jSignPdf->setParam($param); $signed = $this->signWrapper($jSignPdf); @@ -625,61 +608,63 @@ private function shouldUseJSignTimestampPlaceholder(string $template): bool { public function getSignatureText(): string { $renderMode = $this->signatureTextService->getRenderMode(); if ($renderMode !== SignerElementsService::RENDER_MODE_GRAPHIC_ONLY) { - $data = $this->parseSignatureText(); - $signatureText = '"' . str_replace( - ['"', '$'], - ['\"', '\$'], - $data['parsed'] - ) . '"'; - } else { - $signatureText = '""'; + return $this->parseSignatureText()['parsed']; } - - return $signatureText; + return ''; } - private function listParamsToString(array $params): string { - $paramString = ''; - foreach ($params as $flag => $value) { - $paramString .= ' ' . $flag; - if ($value !== null && $value !== '') { - $paramString .= ' ' . $value; + /** + * Options with a null value are flags. Every other value reaches the + * wrapper as a string; the wrapper escapes it for the shell. + * + * @param array $params + * @return array + */ + private function toJSignParameters(array $params): array { + $parameters = []; + foreach ($params as $option => $value) { + if ($value === null) { + $parameters[] = $option; + continue; } + $parameters[$option] = (string)$value; } - return $paramString; + return $parameters; } + /** + * @return array + */ private function getTsaParameters(): array { $tsaSettings = $this->getTsaSettings(); - $tsaUrl = $tsaSettings['url']; - if (empty($tsaUrl)) { + if (empty($tsaSettings['url'])) { return []; } - $params = [ - '--tsa-server-url' => $tsaUrl, - '--tsa-policy-oid' => $tsaSettings['policy_oid'], - ]; - - if (!$params['--tsa-policy-oid']) { - unset($params['--tsa-policy-oid']); + $params = ['--tsa-server-url' => $tsaSettings['url']]; + if ($tsaSettings['policy_oid']) { + $params['--tsa-policy-oid'] = $tsaSettings['policy_oid']; } - - $tsaAuthType = $tsaSettings['auth_type']; - if ($tsaAuthType === 'basic') { - $tsaUsername = $tsaSettings['username']; - $tsaPassword = $this->appConfig->getValueString(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY, ''); - - if (!empty($tsaUsername) && !empty($tsaPassword)) { - $params['--tsa-authentication'] = 'PASSWORD'; - $params['--tsa-user'] = $tsaUsername; - $params['--tsa-password'] = $tsaPassword; - } + if ($this->getTsaPassword() !== '') { + $params['--tsa-authentication'] = 'PASSWORD'; + $params['--tsa-user'] = $tsaSettings['username']; } return $params; } + /** + * The TSA password never goes to the command line: the wrapper writes it + * to the stdin of JSignPdf. + */ + private function getTsaPassword(): string { + $tsaSettings = $this->getTsaSettings(); + if (empty($tsaSettings['url']) || $tsaSettings['auth_type'] !== 'basic' || empty($tsaSettings['username'])) { + return ''; + } + return $this->appConfig->getValueString(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY, ''); + } + /** * @return array{url: string, policy_oid: string, auth_type: string, username: string} */ diff --git a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php index 3e52ef5fb6..2f97a4794c 100644 --- a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php @@ -24,6 +24,7 @@ use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicy; use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicyValue; use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicy; +use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicyValue; use OCA\Libresign\Service\SignatureBackgroundService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; @@ -319,7 +320,7 @@ public function testSignAffectedParams( float $templateFontSize, string $pdfContent, ?string $hashAlgorithm, - string $params, + array $params, ):void { if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { $this->markTestSkipped('Certificate initialization failed'); @@ -328,7 +329,12 @@ public function testSignAffectedParams( $inputFile = $this->createMock(\OC\Files\Node\File::class); $inputFile->method('getContent') ->willReturn($pdfContent); + $paramsSeen = []; $mock = $this->createMock(JSignPDF::class); + $mock->method('setParam') + ->willReturnCallback(function (JSignParam $param) use (&$paramsSeen): void { + $paramsSeen[] = $param->getJSignParameters(); + }); $mock->method('sign')->willReturn('content'); $this->signatureBackgroundService->method('getSignatureBackgroundType')->willReturn( @@ -351,7 +357,7 @@ public function testSignAffectedParams( $this->persistHashAlgorithmPolicy($hashAlgorithm ?? ''); $this->appConfig->setValueString('libresign', 'java_path', __FILE__); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); $jSignPdfHandler = $this->getInstance(); $jSignPdfHandler->setVisibleElements($visibleElements); @@ -362,15 +368,28 @@ public function testSignAffectedParams( $jSignPdfHandler->setPassword('password'); $actual = $jSignPdfHandler->getSignedContent(); $this->assertEquals('content', $actual); - $jSignParam = $jSignPdfHandler->getJSignParam(); - $this->assertEquals('password', $jSignParam->getPassword()); - $paramsAsOptions = $jSignParam->getJSignParameters(); - $paramsAsOptions = preg_replace('/\\/\S+_merged.png/', 'merged.png', $paramsAsOptions); + $this->assertEquals('password', $jSignPdfHandler->getJSignParam()->getPassword()); + $this->assertCount(1, $paramsSeen); + $paramsAsOptions = preg_replace('/\\/\S+_merged.png/', 'merged.png', $paramsSeen[0]); $paramsAsOptions = preg_replace('/\\/\S+_text_image.png/', 'text_image.png', (string)$paramsAsOptions); $paramsAsOptions = preg_replace('/\\/\S+_background.png/', 'background.png', (string)$paramsAsOptions); $paramsAsOptions = preg_replace('/\\/\S+app-dark.png/', 'signature.png', (string)$paramsAsOptions); - $paramsAsOptions = preg_replace('/ --tsa-server-url\s+\S+/', '', (string)$paramsAsOptions); - $this->assertEquals($params, $paramsAsOptions); + $this->assertSame(self::expectedJSignParameters($params), $paramsAsOptions); + } + + /** + * What JSignParam::getJSignParameters() renders: the wrapper defaults + * followed by the given options, each option and value escaped for the + * shell, flags as bare escaped tokens. + */ + private static function expectedJSignParameters(array $params): string { + $tokens = []; + foreach (array_merge(['-a', '-kst' => 'PKCS12'], $params) as $option => $value) { + $tokens[] = is_string($option) + ? escapeshellarg($option) . ' ' . escapeshellarg($value) + : escapeshellarg($value); + } + return implode(' ', $tokens); } public static function providerSignAffectedParams(): array { @@ -385,7 +404,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 0, 'pdfContent' => '%PDF-1', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA1', + 'params' => ['--hash-algorithm' => 'SHA1'], ], 'page = 1 is default, do not will set the page' => [ 'visibleElements' => [self::getElement([ @@ -403,7 +422,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -llx 0 -lly 0 -urx 0 -ury 0 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-llx' => '0', '-lly' => '0', '-urx' => '0', '-ury' => '0', '--bg-path' => 'merged.png'] ], 'page != 1: will have pg; without template: l2-text empty' => [ 'visibleElements' => [self::getElement([ @@ -421,7 +440,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'] ], 'with template we have the l2-text' => [ 'visibleElements' => [self::getElement([ @@ -439,7 +458,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path background.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'background.png', '--hash-algorithm' => 'SHA256'] ], 'font size != default font size: emits --font-size' => [ 'visibleElements' => [self::getElement([ @@ -457,7 +476,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 11, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --font-size 11 --bg-path background.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--font-size' => '11', '--bg-path' => 'background.png', '--hash-algorithm' => 'SHA256'] ], 'background = deleted: bg-path = signature' => [ 'visibleElements' => [self::getElement([ @@ -475,7 +494,43 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path signature.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'signature.png', '--hash-algorithm' => 'SHA256'] + ], + 'template with shell special characters reaches the wrapper unescaped' => [ + 'visibleElements' => [self::getElement([ + 'page' => 2, + 'llx' => 10, + 'lly' => 20, + 'urx' => 30, + 'ury' => 40, + ], realpath(__DIR__ . '/../../../../../img/app-dark.png'))], + 'signatureWidth' => 20, + 'signatureHeight' => 20, + 'template' => 'a"b $c \'d e', + 'signatureBackgroundType' => 'deleted', + 'renderMode' => SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY, + 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, + 'pdfContent' => '%PDF-1.6', + 'hashAlgorithm' => '', + 'params' => ['--l2-text' => 'a"b $c \'d e', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'signature.png', '--hash-algorithm' => 'SHA256'], + ], + 'font size != default but no template: no --font-size' => [ + 'visibleElements' => [self::getElement([ + 'page' => 2, + 'llx' => 10, + 'lly' => 20, + 'urx' => 30, + 'ury' => 40, + ], realpath(__DIR__ . '/../../../../../img/app-dark.png'))], + 'signatureWidth' => 20, + 'signatureHeight' => 20, + 'template' => '', + 'signatureBackgroundType' => 'default', + 'renderMode' => SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY, + 'templateFontSize' => 11, + 'pdfContent' => '%PDF-1.6', + 'hashAlgorithm' => '', + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'], ], 'background and template, bg-path = background, img-path = signature' => [ 'visibleElements' => [self::getElement([ @@ -493,7 +548,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --bg-path background.png --img-path signature.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--bg-path' => 'background.png', '--img-path' => 'signature.png', '--hash-algorithm' => 'SHA256'] ], 'background and template, render mode equals to SIGNAME_AND_DESCRIPTION: bg-path = background, img-path = text_image' => [ 'visibleElements' => [self::getElement([ @@ -511,7 +566,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 1 -lly 100 -urx 351 -ury 200 --render-mode GRAPHIC_AND_DESCRIPTION --bg-path background.png --img-path text_image.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '1', '-lly' => '100', '-urx' => '351', '-ury' => '200', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--bg-path' => 'background.png', '--img-path' => 'text_image.png', '--hash-algorithm' => 'SHA256'] ], 'template without background; with signature image; render-mode: SIGNAME_AND_DESCRIPTION' => [ 'visibleElements' => [self::getElement([ @@ -529,7 +584,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --img-path text_image.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--img-path' => 'text_image.png', '--hash-algorithm' => 'SHA256'] ], 'template without background; without signature image; render-mode: SIGNAME_AND_DESCRIPTION' => [ 'visibleElements' => [self::getElement([ @@ -547,7 +602,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --img-path text_image.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--img-path' => 'text_image.png', '--hash-algorithm' => 'SHA256'] ], // Regression: background with GRAPHIC_AND_DESCRIPTION but NO user signature image. // Before the fix, mergeBackgroundWithSignature('...', '') crashed with new Imagick(''). @@ -568,7 +623,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --bg-path background.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--bg-path' => 'background.png', '--hash-algorithm' => 'SHA256'] ], 'background without template: bg-path = merged with signature, without img-path' => [ 'visibleElements' => [self::getElement([ @@ -586,7 +641,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'] ], 'regression: invalid stored dimensions should fallback to defaults and keep signing flow' => [ 'visibleElements' => [self::getElement([ @@ -604,7 +659,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'] ], ]; } @@ -626,7 +681,7 @@ public function testDocMdpAppliedOnlyOnFirstVisibleElement(): void { $this->persistHashAlgorithmPolicy(''); $this->appConfig->setValueString('libresign', 'java_path', __FILE__); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); $paramsSeen = []; $mock = $this->createMock(JSignPDF::class); @@ -668,8 +723,8 @@ public function testDocMdpAppliedOnlyOnFirstVisibleElement(): void { $jSignPdfHandler->getSignedContent(); $this->assertCount(2, $paramsSeen); - $this->assertStringContainsString(' -cl ' . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, $paramsSeen[0]); - $this->assertStringNotContainsString(' -cl ' . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, $paramsSeen[1]); + $this->assertStringContainsString("'-cl' '" . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name . "'", $paramsSeen[0]); + $this->assertStringNotContainsString("'-cl' '" . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name . "'", $paramsSeen[1]); } public function testDocMdpSkippedWhenSignatureExists(): void { @@ -689,7 +744,7 @@ public function testDocMdpSkippedWhenSignatureExists(): void { $this->persistHashAlgorithmPolicy(''); $this->appConfig->setValueString('libresign', 'java_path', __FILE__); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); $paramsSeen = []; $mock = $this->createMock(JSignPDF::class); @@ -724,7 +779,7 @@ public function testDocMdpSkippedWhenSignatureExists(): void { $jSignPdfHandler->getSignedContent(); $this->assertCount(1, $paramsSeen); - $this->assertStringNotContainsString(' -cl ' . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, $paramsSeen[0]); + $this->assertStringNotContainsString("'-cl' '" . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name . "'", $paramsSeen[0]); } #[DataProvider('providerSignatureDimensions')] @@ -823,44 +878,43 @@ private static function getElement(array $attributes = [], string $imagePath = ' } #[DataProvider('providerGetJSignParam')] - public function testGetJSignParam(string $temp_path, string $java_path, string $jar_path, bool $throwException): void { + public function testGetJSignParam(string $temp_path, string $java_path, string $jsignpdf_path, bool $throwException): void { $this->appConfig->setValueString('libresign', 'jsignpdf_home', '/'); $this->appConfig->setValueString('libresign', 'java_path', $java_path); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', $temp_path); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', $jar_path); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', $jsignpdf_path); $this->javaHelper->method('getJavaPath')->willReturn($java_path); - $expected = new JSignParam(); - if ($java_path) { - $expected->setJavaPath("JSIGNPDF_HOME='/' $java_path -Duser.home='/' "); - } - $expected->setTempPath($temp_path); - $expected->setjSignPdfJarPath($jar_path); - $jSignPdfHandler = $this->getInstance(); if ($throwException) { $this->expectException(\Exception::class); - $jSignParam = $jSignPdfHandler->getJSignParam(); + $jSignPdfHandler->getJSignParam(); + return; + } + $jSignParam = $jSignPdfHandler->getJSignParam(); + $this->assertSame('', $jSignParam->getPdf()); + if ($java_path === '') { + $this->assertTrue($jSignParam->isUseJavaInstalled()); } else { - $jSignParam = $jSignPdfHandler->getJSignParam(); - $this->assertEquals($expected->getPdf(), $jSignParam->getPdf()); - $this->assertEquals($expected->getJavaPath(), $jSignParam->getJavaPath()); - $this->assertEquals($expected->getTempPath(), $jSignParam->getTempPath()); - $this->assertEquals($expected->getjSignPdfJarPath(), $jSignParam->getjSignPdfJarPath()); - $this->assertEquals('-a -kst PKCS12', $jSignParam->getJSignParameters()); + $this->assertFalse($jSignParam->isUseJavaInstalled()); + $this->assertSame($java_path, $jSignParam->getJavaPath()); } + $this->assertSame($temp_path, $jSignParam->getTempPath()); + $this->assertSame($jsignpdf_path, $jSignParam->getJSignPdfPath()); + $this->assertSame(['-Duser.home=/'], $jSignParam->getJavaOptions()); + $this->assertSame(['JSIGNPDF_HOME' => '/'], $jSignParam->getEnvironmentVariables()); + $this->assertSame("'-a' '-kst' 'PKCS12'", $jSignParam->getJSignParameters()); } public static function providerGetJSignParam(): array { return [ - ['', '', __FILE__, true], - ['invalid', '', __FILE__, true], - [sys_get_temp_dir(), '', __FILE__, false], - [sys_get_temp_dir(), 'b', __FILE__, true], - [sys_get_temp_dir(), __FILE__, __FILE__, false], - [sys_get_temp_dir(), 'b', __FILE__, true], - [sys_get_temp_dir(), __FILE__, __FILE__, false], - [sys_get_temp_dir(), __FILE__, '', true], + 'temp path empty' => ['', '', __DIR__, true], + 'temp path not writable' => ['invalid', '', __DIR__, true], + 'system java' => [sys_get_temp_dir(), '', __DIR__, false], + 'java binary missing' => [sys_get_temp_dir(), 'b', __DIR__, true], + 'downloaded java' => [sys_get_temp_dir(), __FILE__, __DIR__, false], + 'jsignpdf path not configured' => [sys_get_temp_dir(), __FILE__, '', true], + 'jsignpdf path is a file, not the extracted directory' => [sys_get_temp_dir(), __FILE__, __FILE__, true], ]; } @@ -881,7 +935,7 @@ public function testGetSignatureTextWithTwigDateFilterAndTimezone(): void { $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} [A-Z]{3,4}"$/', $actual); + $this->assertMatchesRegularExpression('/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} [A-Z]{3,4}$/', $actual); } public function testGetSignatureTextWithTwigDateFilterWithoutTimezone(): void { @@ -893,7 +947,7 @@ public function testGetSignatureTextWithTwigDateFilterWithoutTimezone(): void { $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4}"$/', $actual); + $this->assertMatchesRegularExpression('/^\d{2}\/\d{2}\/\d{4}$/', $actual); } public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEmpty(): void { @@ -905,25 +959,27 @@ public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEm $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertSame('""', $actual); + $this->assertSame('', $actual); } public static function providerGetSignatureText(): array { return [ - [SignerElementsService::RENDER_MODE_DEFAULT, '', '""'], - [SignerElementsService::RENDER_MODE_DEFAULT, 'a', '"a"'], - [SignerElementsService::RENDER_MODE_DEFAULT, "a\na", "\"a\na\""], - [SignerElementsService::RENDER_MODE_DEFAULT, 'a"a', '"a\"a"'], - [SignerElementsService::RENDER_MODE_DEFAULT, 'a$a', '"a\$a"'], + // The text reaches the wrapper as is: the wrapper escapes it for the shell. + [SignerElementsService::RENDER_MODE_DEFAULT, '', ''], + [SignerElementsService::RENDER_MODE_DEFAULT, 'a', 'a'], + [SignerElementsService::RENDER_MODE_DEFAULT, "a\na", "a\na"], + [SignerElementsService::RENDER_MODE_DEFAULT, 'a"a', 'a"a'], + [SignerElementsService::RENDER_MODE_DEFAULT, "a'a", "a'a"], + [SignerElementsService::RENDER_MODE_DEFAULT, 'a$a', 'a$a'], // Plain {{ServerSignatureDate}} (no spaces) preserves JSign placeholder - [SignerElementsService::RENDER_MODE_DEFAULT, '{{ServerSignatureDate}}', '"\${timestamp}"'], + [SignerElementsService::RENDER_MODE_DEFAULT, '{{ServerSignatureDate}}', '${timestamp}'], // Plain {{ ServerSignatureDate }} (with spaces) also preserves JSign placeholder - [SignerElementsService::RENDER_MODE_DEFAULT, '{{ ServerSignatureDate }}', '"\${timestamp}"'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, '', '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a', '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, "a\na", '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a"a', '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a$a', '""'], + [SignerElementsService::RENDER_MODE_DEFAULT, '{{ ServerSignatureDate }}', '${timestamp}'], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, '', ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a', ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, "a\na", ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a"a', ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a$a', ''], ]; } @@ -948,4 +1004,209 @@ public function testCheckTsaErrorUnknownHostMentionsDnsNetworkFirewall(): void { 'TSAClientBouncyCastle: java.net.UnknownHostException: invalid-tsa.example.com', ]); } + + #[DataProvider('providerTsaParameters')] + public function testTsaParametersAndPassword(array $tsaSettings, string $storedPassword, array $expectedParameters, array $expectedPasswords): void { + if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { + $this->markTestSkipped('Certificate initialization failed'); + } + $this->resolvedPolicyValues[TsaPolicy::KEY] = TsaPolicyValue::encode($tsaSettings); + $this->appConfig->setValueString('libresign', TsaPolicy::PASSWORD_APP_CONFIG_KEY, $storedPassword); + $this->appConfig->setValueString('libresign', 'java_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); + $this->persistHashAlgorithmPolicy('SHA256'); + + $inputFile = $this->createMock(\OC\Files\Node\File::class); + $inputFile->method('getContent')->willReturn('%PDF-1.6'); + + $paramsSeen = []; + $mock = $this->createMock(JSignPDF::class); + $mock->method('setParam') + ->willReturnCallback(function (JSignParam $param) use (&$paramsSeen): void { + $paramsSeen[] = $param; + }); + $mock->method('sign')->willReturn('content'); + + $jSignPdfHandler = $this->getInstance(); + $jSignPdfHandler->setJSignPdf($mock); + $jSignPdfHandler->setInputFile($inputFile); + $jSignPdfHandler->setCertificate(self::$certificateContent); + $jSignPdfHandler->setPassword('password'); + $jSignPdfHandler->getSignedContent(); + + $this->assertCount(1, $paramsSeen); + $this->assertSame( + self::expectedJSignParameters($expectedParameters + ['--hash-algorithm' => 'SHA256']), + $paramsSeen[0]->getJSignParameters(), + ); + $this->assertSame($expectedPasswords, $paramsSeen[0]->getPasswords()); + if ($storedPassword !== '') { + $this->assertStringNotContainsString($storedPassword, $paramsSeen[0]->getJSignParameters()); + } + } + + public static function providerTsaParameters(): array { + $tsa = [ + 'url' => 'https://tsa.example.test/tsr', + 'policy_oid' => '1.2.3.4', + 'auth_type' => 'basic', + 'username' => 'alice', + ]; + return [ + 'no TSA configured' => [ + ['url' => ''], + 'tsa secret', + [], + [], + ], + 'url only' => [ + ['url' => 'https://tsa.example.test/tsr'], + '', + ['--tsa-server-url' => 'https://tsa.example.test/tsr'], + [], + ], + 'url with policy OID and no authentication' => [ + ['url' => 'https://tsa.example.test/tsr', 'policy_oid' => '1.2.3.4', 'auth_type' => 'none'], + 'tsa secret', + ['--tsa-server-url' => 'https://tsa.example.test/tsr', '--tsa-policy-oid' => '1.2.3.4'], + [], + ], + 'basic authentication: user on the command line, password over stdin' => [ + $tsa, + 'tsa secret', + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--tsa-policy-oid' => '1.2.3.4', + '--tsa-authentication' => 'PASSWORD', + '--tsa-user' => 'alice', + ], + ['-tsp' => 'tsa secret'], + ], + 'basic authentication with shell characters in the password' => [ + $tsa, + "p4\$s 'w\"ord", + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--tsa-policy-oid' => '1.2.3.4', + '--tsa-authentication' => 'PASSWORD', + '--tsa-user' => 'alice', + ], + ['-tsp' => "p4\$s 'w\"ord"], + ], + 'basic authentication without a stored password is skipped' => [ + $tsa, + '', + ['--tsa-server-url' => 'https://tsa.example.test/tsr', '--tsa-policy-oid' => '1.2.3.4'], + [], + ], + 'basic authentication without a URL sends nothing' => [ + ['url' => '', 'auth_type' => 'basic', 'username' => 'alice'], + 'tsa secret', + [], + [], + ], + 'basic authentication without a username is skipped' => [ + ['url' => 'https://tsa.example.test/tsr', 'auth_type' => 'basic', 'username' => ''], + 'tsa secret', + ['--tsa-server-url' => 'https://tsa.example.test/tsr'], + [], + ], + ]; + } + + #[DataProvider('providerCertificationLevelWithoutVisibleElements')] + public function testCertificationLevelWithoutVisibleElements(bool $docMdpEnabled, array $visibleElements, string $pdfContent, array $tsaSettings, array $expectedParameters): void { + if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { + $this->markTestSkipped('Certificate initialization failed'); + } + $this->resolvedPolicyValues[TsaPolicy::KEY] = TsaPolicyValue::encode($tsaSettings); + $this->appConfig->setValueString('libresign', 'java_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); + $this->persistSignatureStampPolicy('', SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY, 10, SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 100, 100, 'deleted'); + $this->persistHashAlgorithmPolicy('SHA256'); + $this->signatureBackgroundService->method('getSignatureBackgroundType')->willReturn('deleted'); + + $inputFile = $this->createMock(\OC\Files\Node\File::class); + $inputFile->method('getContent')->willReturn($pdfContent); + + $paramsSeen = []; + $mock = $this->createMock(JSignPDF::class); + $mock->method('setParam') + ->willReturnCallback(function (JSignParam $param) use (&$paramsSeen): void { + $paramsSeen[] = $param->getJSignParameters(); + }); + $mock->method('sign')->willReturn('content'); + + $docMdpConfigService = $this->createMock(DocMdpConfigService::class); + $docMdpConfigService->method('isEnabled')->willReturn($docMdpEnabled); + $docMdpConfigService->method('getLevel')->willReturn(DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS); + + $jSignPdfHandler = $this->getInstance(); + $this->setDocMdpConfigService($jSignPdfHandler, $docMdpConfigService); + $jSignPdfHandler->setVisibleElements($visibleElements); + $jSignPdfHandler->setJSignPdf($mock); + $jSignPdfHandler->setInputFile($inputFile); + $jSignPdfHandler->setSignatureParams(['SignerCommonName' => 'Test User']); + $jSignPdfHandler->setCertificate(self::$certificateContent); + $jSignPdfHandler->setPassword('password'); + $jSignPdfHandler->getSignedContent(); + + $this->assertCount(1, $paramsSeen); + $paramsAsOptions = preg_replace('/\\/\S+app-dark.png/', 'signature.png', $paramsSeen[0]); + $this->assertSame(self::expectedJSignParameters($expectedParameters), $paramsAsOptions); + } + + public static function providerCertificationLevelWithoutVisibleElements(): array { + $element = self::getElement([ + 'page' => 1, + 'llx' => 10, + 'lly' => 10, + 'urx' => 110, + 'ury' => 60, + ], realpath(__DIR__ . '/../../../../../img/app-dark.png')); + $tsa = ['url' => 'https://tsa.example.test/tsr']; + return [ + 'certification before the TSA options when the PDF has no signature' => [ + true, + [], + '%PDF-1.6', + $tsa, + ['-cl' => DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, '--tsa-server-url' => 'https://tsa.example.test/tsr', '--hash-algorithm' => 'SHA256'], + ], + 'no certification when the PDF already has a signature' => [ + true, + [], + "%PDF-1.6\n/ByteRange [0 0 0 0]", + $tsa, + ['--tsa-server-url' => 'https://tsa.example.test/tsr', '--hash-algorithm' => 'SHA256'], + ], + 'no certification when DocMDP is disabled, even with a visible element on a signed PDF' => [ + false, + [$element], + "%PDF-1.6\n/ByteRange [0 0 0 0]", + ['url' => ''], + ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-llx' => '10', '-lly' => '10', '-urx' => '110', '-ury' => '60', '--bg-path' => 'signature.png'], + ], + ]; + } + + #[DataProvider('providerToJSignParameters')] + public function testToJSignParameters(array $params, array $expected): void { + $jSignPdfHandler = $this->getInstance(); + + $this->assertSame($expected, self::invokePrivate($jSignPdfHandler, 'toJSignParameters', [$params])); + } + + public static function providerToJSignParameters(): array { + return [ + 'null is a flag' => [['-V' => null], ['-V']], + 'integers become strings' => [['-pg' => 2, '-llx' => 0], ['-pg' => '2', '-llx' => '0']], + 'floats become strings' => [['--font-size' => 16.5, '--bg-scale' => 1.0], ['--font-size' => '16.5', '--bg-scale' => '1']], + 'empty string is a value' => [['--l2-text' => ''], ['--l2-text' => '']], + 'text is kept as is' => [['--l2-text' => 'a"b $c \'d'], ['--l2-text' => 'a"b $c \'d']], + 'order is preserved' => [['-a' => null, '-kst' => 'PKCS12', '-cl' => 'CERTIFIED_NO_CHANGES_ALLOWED'], ['-a', '-kst' => 'PKCS12', '-cl' => 'CERTIFIED_NO_CHANGES_ALLOWED']], + ]; + } } From 16e09112f0f2f8146f4cb01410effb369fa99cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maia?= Date: Fri, 4 Sep 2026 22:49:40 -0300 Subject: [PATCH 4/8] test(jsignpdf): generate the CA identifier before creating the test certificate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generateRootCert() resolves the CRL distribution point from the CA identifier. Without one the certificate generation failed and 68 tests of this class were skipped as "Certificate initialization failed". Signed-off-by: André Maia Assisted-by: Claude Code:claude-fable-5-1 --- tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php index 2f97a4794c..b9cbd48ede 100644 --- a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php @@ -16,6 +16,7 @@ use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\SignEngine\JSignPdfHandler; use OCA\Libresign\Helper\JavaHelper; +use OCA\Libresign\Service\CaIdentifierService; use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService; use OCA\Libresign\Service\Policy\Model\ResolvedPolicy; use OCA\Libresign\Service\Policy\PolicyService; @@ -61,6 +62,8 @@ public static function setUpBeforeClass(): void { try { $appConfig = self::getMockAppConfig(); $appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl'); + // The CRL distribution point of the root certificate needs a CA identifier. + \OCP\Server::get(CaIdentifierService::class)->generateCaId('openssl'); self::$certificateEngineFactory = \OCP\Server::get(CertificateEngineFactory::class); $certificateEngine = self::$certificateEngineFactory->getEngine(); $certificateEngine From ea04380fcd0b87e37cf733564b9f7651d70c08d6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:53:05 -0300 Subject: [PATCH 5/8] test: reuse JSignPdf version in release tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Service/Install/JSignPdfReleaseTest.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php b/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php index 24d63fe394..a647f6fb14 100644 --- a/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php +++ b/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php @@ -13,23 +13,26 @@ use PHPUnit\Framework\TestCase; final class JSignPdfReleaseTest extends TestCase { - private const RELEASE_URL = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_3_1_0/'; + private static function releaseUrl(): string { + return 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' + . str_replace('.', '_', JSignPdfRelease::VERSION) . '/'; + } public function testArchiveIsTheMinimalPackageOfTheVersion(): void { $this->assertSame('3.1.0', JSignPdfRelease::VERSION); - $this->assertSame('jsignpdf-3.1.0-minimal.zip', JSignPdfRelease::archiveName()); + $this->assertSame('jsignpdf-' . JSignPdfRelease::VERSION . '-minimal.zip', JSignPdfRelease::archiveName()); } public function testDownloadUrlPointsToTheArchiveOfTheReleaseTag(): void { $this->assertSame( - self::RELEASE_URL . 'jsignpdf-3.1.0-minimal.zip', + self::releaseUrl() . 'jsignpdf-' . JSignPdfRelease::VERSION . '-minimal.zip', JSignPdfRelease::downloadUrl(), ); } public function testChecksumUrlPointsToTheSha256SumsOfTheSameRelease(): void { $this->assertSame( - self::RELEASE_URL . 'jsignpdf-3.1.0-SHA256SUMS.txt', + self::releaseUrl() . 'jsignpdf-' . JSignPdfRelease::VERSION . '-SHA256SUMS.txt', JSignPdfRelease::checksumUrl(), ); } @@ -43,15 +46,15 @@ public static function providerInstallPath(): array { return [ 'appdata folder' => [ '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf', - '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf/jsignpdf-3.1.0', + '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION, ], 'path with spaces' => [ '/opt/libre sign/jsignpdf', - '/opt/libre sign/jsignpdf/jsignpdf-3.1.0', + '/opt/libre sign/jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION, ], 'relative path' => [ 'jsignpdf', - 'jsignpdf/jsignpdf-3.1.0', + 'jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION, ], ]; } From ed5dec5155af9a654b0832f954d0c53a47b8f37c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:53:14 -0300 Subject: [PATCH 6/8] test: reuse JSignPdf version in setup service tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/Install/SignSetupServiceTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/php/Unit/Service/Install/SignSetupServiceTest.php b/tests/php/Unit/Service/Install/SignSetupServiceTest.php index 37d33a1567..7ff050b1df 100644 --- a/tests/php/Unit/Service/Install/SignSetupServiceTest.php +++ b/tests/php/Unit/Service/Install/SignSetupServiceTest.php @@ -12,6 +12,7 @@ use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Service\Install\JSignPdfRelease; use OCA\Libresign\Service\Install\SignSetupService; use OCP\App\IAppManager; use OCP\Files\AppData\IAppDataFactory; @@ -199,7 +200,7 @@ public function testVerify(): void { #[DataProvider('dataGetInstallPath')] public function testGetInstallPath(string $architecture, string $resource, string $distro, string $expected): void { $this->appConfig->setValueString(Application::APP_ID, 'java_path', 'vfs://home/data/appdata_1/libresign/x86_64/linux/java/jdk-21.0.2+13-jre/bin/java'); - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', 'vfs://home/data/appdata_1/libresign/x86_64/jsignpdf/jsignpdf-3.1.0'); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', 'vfs://home/data/appdata_1/libresign/x86_64/jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION); $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', 'vfs://home/data/appdata_1/libresign/x86_64/pdftk/pdftk.jar'); $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', 'vfs://home/data/appdata_1/libresign/x86_64/cfssl/cfssl'); $actual = $this->getInstance() From 9566205a2ba8bfd83e24b5e0547c23ce00f9ff55 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:53:21 -0300 Subject: [PATCH 7/8] test: reuse JSignPdf version in setup check tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Unit/SetupCheck/JSignPdfSetupCheckTest.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php b/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php index 94a73f3166..9e56a4fdf3 100644 --- a/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php +++ b/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php @@ -25,6 +25,7 @@ function is_dir(string $filename): bool { use OCA\Libresign\Handler\SignEngine\JSignPdfHandler; use OCA\Libresign\Helper\JavaHelper; use OCA\Libresign\Service\Install\InstallService; +use OCA\Libresign\Service\Install\JSignPdfRelease; use OCA\Libresign\Service\Install\SignSetupService; use OCA\Libresign\SetupCheck\JSignPdfSetupCheck; use OCA\Libresign\Tests\Mock\FileSystemMock; @@ -140,7 +141,7 @@ public function testRunVerifyFails(): void { public function testRunBinaryNotFound(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') @@ -161,7 +162,7 @@ public function testRunBinaryNotFound(): void { public function testRunJavaNotFound(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') @@ -183,7 +184,7 @@ public function testRunJavaNotFound(): void { public function testRunJavaBinaryMissing(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') @@ -212,7 +213,7 @@ private function createJSignParamMock() { public function testRunVersionEmpty(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') @@ -243,7 +244,7 @@ public function testRunVersionEmpty(): void { public function testRunVersionTooLow(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') @@ -273,7 +274,7 @@ public function testRunVersionTooLow(): void { public function testRunVersionTooHigh(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') @@ -303,7 +304,7 @@ public function testRunVersionTooHigh(): void { public function testRunSuccess(): void { $this->mockTranslation(); - $jsignPdfPath = '/fake/path/jsignpdf-3.1.0'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') From 3c7427310aaeb8afa7d9598f6db3cbac193edc48 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:07:15 -0300 Subject: [PATCH 8/8] fix: update PDF signature validation return type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/Signature/PdfSignatureValidationService.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 2a222deb4c..e27b077b85 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -10,6 +10,8 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ExtractedSignature; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\TimestampToken; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; @@ -103,14 +105,14 @@ public function validateFromString(string $pdfContent): array { /** * @param resource $resource - * @return list, certificateValidation: ValidationResult}> + * @return list, certificateValidation: ValidationResult, timestamp: TimestampToken|null}> */ protected function validateNativeFromResource($resource): array { return $this->validator->validateFromResource($resource); } /** - * @return list, certificateValidation: ValidationResult}> + * @return list, certificateValidation: ValidationResult, timestamp: TimestampToken|null}> */ protected function validateNativeFromString(string $pdfContent): array { return $this->validator->validateFromString($pdfContent);