Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty
Submodule 3rdparty updated 38 files
+2 −2 composer.json
+13 −13 composer.lock
+1 −1 composer/composer/autoload_classmap.php
+1 −1 composer/composer/autoload_static.php
+14 −14 composer/composer/installed.json
+53 −0 composer/jsignpdf/jsignpdf-php/AGENTS.md
+15 −0 composer/jsignpdf/jsignpdf-php/Dockerfile
+9 −0 composer/jsignpdf/jsignpdf-php/compose.yml
+3 −2 composer/jsignpdf/jsignpdf-php/composer.json
+10 −10 composer/jsignpdf/jsignpdf-php/composer.lock
+1 −0 composer/jsignpdf/jsignpdf-php/example/index.php
+69 −27 composer/jsignpdf/jsignpdf-php/src/Runtime/JSignPdfRuntimeService.php
+192 −12 composer/jsignpdf/jsignpdf-php/src/Sign/JSignParam.php
+64 −16 composer/jsignpdf/jsignpdf-php/src/Sign/JSignService.php
+67 −0 composer/jsignpdf/jsignpdf-php/tests/Integration/SignPdfTest.php
+364 −7 composer/jsignpdf/jsignpdf-php/tests/JSignPDFTest.php
+223 −0 composer/jsignpdf/jsignpdf-php/tests/Runtime/JSignPdfRuntimeServiceTest.php
+1 −0 composer/jsignpdf/jsignpdf-php/tests/Runtime/JavaRuntimeServiceTest.php
+95 −0 composer/jsignpdf/jsignpdf-php/tests/Sign/JSignParamTest.php
+12 −8 composer/jsignpdf/jsignpdf-php/vendor-bin/coding-standard/composer.lock
+80 −43 composer/jsignpdf/jsignpdf-php/vendor-bin/phpunit/composer.lock
+16 −0 composer/libresign/pdf-signature-validator/src/Model/DocumentModificationState.php
+1 −1 composer/libresign/pdf-signature-validator/src/Model/SignatureMetadata.php
+17 −0 composer/libresign/pdf-signature-validator/src/Model/TimestampToken.php
+3 −0 composer/libresign/pdf-signature-validator/src/Model/ValidationReason.php
+90 −0 composer/libresign/pdf-signature-validator/src/Parser/Asn1NodeReader.php
+43 −0 composer/libresign/pdf-signature-validator/src/Parser/CertificateSubjectExtractor.php
+80 −0 composer/libresign/pdf-signature-validator/src/Parser/CmsTimestampExtractor.php
+163 −0 composer/libresign/pdf-signature-validator/src/Parser/PdfDocumentModificationAnalyzer.php
+35 −20 composer/libresign/pdf-signature-validator/src/Parser/PdfSignatureExtractor.php
+38 −8 composer/libresign/pdf-signature-validator/src/Parser/PdfSignatureValidator.php
+116 −0 composer/libresign/pdf-signature-validator/src/Parser/TstInfoParser.php
+39 −0 composer/libresign/pdf-signature-validator/tests/Unit/Parser/CmsTimestampExtractorTest.php
+113 −0 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfDocumentModificationAnalyzerTest.php
+43 −1 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureExtractorTest.php
+96 −0 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureStructureValidationTest.php
+16 −5 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureValidatorTest.php
+0 −1 composer/libresign/pdf-signature-validator/vendor-bin/update/composer.json
138 changes: 72 additions & 66 deletions lib/Handler/SignEngine/JSignPdfHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\SignatureBackgroundService;
use OCA\Libresign\Service\SignatureTextService;
use OCA\Libresign\Service\SignerElementsService;
Expand Down Expand Up @@ -77,34 +76,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:
Expand Down Expand Up @@ -256,36 +250,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);
Expand All @@ -304,7 +290,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'],
Expand All @@ -313,7 +299,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg
}

$fontSize = $this->parseSignatureText()['templateFontSize'];
if ($fontSize === self::SIGNATURE_DEFAULT_FONT_SIZE || !$fontSize || $params['--l2-text'] === '""') {
if ($fontSize === self::SIGNATURE_DEFAULT_FONT_SIZE || !$fontSize || $params['--l2-text'] === '') {
$fontSize = 0;
}

Expand All @@ -326,11 +312,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++;
Expand Down Expand Up @@ -365,7 +349,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,
Expand Down Expand Up @@ -400,16 +384,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);
Expand Down Expand Up @@ -616,30 +599,33 @@ 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<string, string|int|float|null> $params
* @return array<array-key, string>
*/
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<string, string>
*/
private function getTsaParameters(): array {
$tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', '');
if (empty($tsaUrl)) {
Expand All @@ -663,13 +649,33 @@ private function getTsaParameters(): array {
if (!empty($tsaUsername) && !empty($tsaPassword)) {
$params['--tsa-authentication'] = 'PASSWORD';
$params['--tsa-user'] = $tsaUsername;
$params['--tsa-password'] = $tsaPassword;
}
}

return $params;
}

private function getTsaPassword(): string {
$tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', '');
if ($tsaUrl === '') {
return '';
}

$tsaAuthType = $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none');
if ($tsaAuthType !== 'basic') {
return '';
}

$tsaUsername = $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', '');
$tsaPassword = $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', '');

if ($tsaUsername === '' || $tsaPassword === '') {
return '';
}

return $tsaPassword;
}

private function signWrapper(JSignPDF $jSignPDF): string {
try {
return $jSignPDF->sign();
Expand Down
26 changes: 13 additions & 13 deletions lib/Service/Install/InstallService.php
Original file line number Diff line number Diff line change
Expand Up @@ -477,36 +477,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();

Expand Down Expand Up @@ -536,8 +534,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');
Expand All @@ -546,6 +545,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');
}
Expand Down
49 changes: 49 additions & 0 deletions lib/Service/Install/JSignPdfRelease.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Libresign\Service\Install;

/**
* Where LibreSign downloads JSignPdf from and where the archive ends up once
* extracted.
*
* JSignPdf 3.1 dropped the single JSignPdf.jar. The "minimal" package, meant
* for headless and command line use, ships a lib/ directory that the PHP
* wrapper starts through the classpath, so the configured path is the
* extracted directory instead of a jar file.
*/
final class JSignPdfRelease {
public const VERSION = '3.1.0';
private const RELEASES_URL = 'https://github.com/intoolswetrust/jsignpdf/releases/download/';

public static function archiveName(): string {
return 'jsignpdf-' . self::VERSION . '-minimal.zip';
}

public static function downloadUrl(): string {
return self::releaseUrl() . self::archiveName();
}

/**
* One line per asset of the release, in the "hash file name" format.
*/
public static function checksumUrl(): string {
return self::releaseUrl() . 'jsignpdf-' . self::VERSION . '-SHA256SUMS.txt';
}

/**
* The archive extracts to a directory named after the version.
*/
public static function installPath(string $extractDir): string {
return $extractDir . '/jsignpdf-' . self::VERSION;
}

private static function releaseUrl(): string {
return self::RELEASES_URL . 'JSignPdf_' . str_replace('.', '_', self::VERSION) . '/';
}
}
4 changes: 2 additions & 2 deletions lib/Service/Install/SignSetupService.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public function getInstallPath(): string {
}
break;
case 'jsignpdf':
$path = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
$path = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path');
if (!$path) {
// fallback
try {
Expand All @@ -203,7 +203,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');
Expand Down
Loading
Loading