Skip to content
2 changes: 1 addition & 1 deletion 3rdparty
Submodule 3rdparty updated 36 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
+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
+151 −0 composer/libresign/pdf-signature-validator/src/Parser/PdfDocumentModificationAnalyzer.php
+35 −20 composer/libresign/pdf-signature-validator/src/Parser/PdfSignatureExtractor.php
+8 −5 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
+16 −5 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureValidatorTest.php
+0 −1 composer/libresign/pdf-signature-validator/vendor-bin/update/composer.json
155 changes: 70 additions & 85 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\Policy\PolicyService;
use OCA\Libresign\Service\Policy\Provider\SignatureHashAlgorithm\SignatureHashAlgorithmPolicy;
use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicyValue;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand All @@ -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'],
Expand All @@ -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;
}

Expand All @@ -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++;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<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 {
$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}
*/
Expand Down
29 changes: 14 additions & 15 deletions lib/Service/Install/InstallService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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');
Expand All @@ -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');
}
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) . '/';
}
}
Loading
Loading