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 66 files
+1 −2 composer.json
+7 −58 composer.lock
+1 −1 composer/composer/autoload_classmap.php
+1 −1 composer/composer/autoload_files.php
+1 −1 composer/composer/autoload_psr4.php
+4 −4 composer/composer/autoload_static.php
+8 −62 composer/composer/installed.json
+0 −661 composer/libresign/pdf-signature-validator/COPYING
+0 −661 composer/libresign/pdf-signature-validator/LICENSES/AGPL-3.0-or-later.txt
+0 −121 composer/libresign/pdf-signature-validator/LICENSES/CC0-1.0.txt
+0 −33 composer/libresign/pdf-signature-validator/REUSE.toml
+0 −69 composer/libresign/pdf-signature-validator/composer.json
+0 −1,369 composer/libresign/pdf-signature-validator/composer.lock
+0 −25 composer/libresign/pdf-signature-validator/deptrac.yaml
+0 −16 composer/libresign/pdf-signature-validator/infection.json5
+0 −21 composer/libresign/pdf-signature-validator/phpmd.xml
+0 −27 composer/libresign/pdf-signature-validator/phpunit.xml
+0 −21 composer/libresign/pdf-signature-validator/psalm.xml
+0 −13 composer/libresign/pdf-signature-validator/rector.php
+0 −11 composer/libresign/pdf-signature-validator/src/Exception/UnsignedPdfException.php
+0 −14 composer/libresign/pdf-signature-validator/src/Model/ExtractedSignature.php
+0 −17 composer/libresign/pdf-signature-validator/src/Model/SignatureMetadata.php
+0 −15 composer/libresign/pdf-signature-validator/src/Model/ValidationReason.php
+0 −16 composer/libresign/pdf-signature-validator/src/Model/ValidationResult.php
+0 −26 composer/libresign/pdf-signature-validator/src/Model/ValidationState.php
+0 −14 composer/libresign/pdf-signature-validator/src/Model/ValidationStatus.php
+0 −78 composer/libresign/pdf-signature-validator/src/Parser/CertificateExtractor.php
+0 −92 composer/libresign/pdf-signature-validator/src/Parser/CertificateInspector.php
+0 −180 composer/libresign/pdf-signature-validator/src/Parser/CertificateValidator.php
+0 −75 composer/libresign/pdf-signature-validator/src/Parser/CmsHashAlgorithmExtractor.php
+0 −148 composer/libresign/pdf-signature-validator/src/Parser/PdfSignatureExtractor.php
+0 −137 composer/libresign/pdf-signature-validator/src/Parser/PdfSignatureValidator.php
+0 −35 composer/libresign/pdf-signature-validator/src/Parser/PopplerStatusMapper.php
+0 −95 composer/libresign/pdf-signature-validator/src/Parser/SignatureValidator.php
+0 −20 composer/libresign/pdf-signature-validator/tests/Fixtures/certs/self-signed-cert.pem
+ composer/libresign/pdf-signature-validator/tests/Fixtures/pdfs/real_jsignpdf_level1.pdf
+ composer/libresign/pdf-signature-validator/tests/Fixtures/pdfs/small_valid-signed.pdf
+ composer/libresign/pdf-signature-validator/tests/Fixtures/pdfs/small_valid.pdf
+0 −56 composer/libresign/pdf-signature-validator/tests/Unit/Parser/CertificateExtractorTest.php
+0 −69 composer/libresign/pdf-signature-validator/tests/Unit/Parser/CertificateInspectorTest.php
+0 −126 composer/libresign/pdf-signature-validator/tests/Unit/Parser/CertificateValidatorTest.php
+0 −18 composer/libresign/pdf-signature-validator/tests/Unit/Parser/CmsHashAlgorithmExtractorTest.php
+0 −177 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureCorpusTest.php
+0 −51 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureExtractorRealPdfTest.php
+0 −55 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureExtractorTest.php
+0 −122 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PdfSignatureValidatorTest.php
+0 −57 composer/libresign/pdf-signature-validator/tests/Unit/Parser/PopplerStatusMapperTest.php
+0 −75 composer/libresign/pdf-signature-validator/tests/Unit/Parser/SignatureValidatorTest.php
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/deptrac/composer.json
+0 −2,124 composer/libresign/pdf-signature-validator/vendor-bin/deptrac/composer.lock
+0 −14 composer/libresign/pdf-signature-validator/vendor-bin/infection/composer.json
+0 −2,624 composer/libresign/pdf-signature-validator/vendor-bin/infection/composer.lock
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/php-cs-fixer/composer.json
+0 −2,764 composer/libresign/pdf-signature-validator/vendor-bin/php-cs-fixer/composer.lock
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/phpmd/composer.json
+0 −1,052 composer/libresign/pdf-signature-validator/vendor-bin/phpmd/composer.lock
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/phpstan/composer.json
+0 −86 composer/libresign/pdf-signature-validator/vendor-bin/phpstan/composer.lock
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/phpunit/composer.json
+0 −1,803 composer/libresign/pdf-signature-validator/vendor-bin/phpunit/composer.lock
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/psalm/composer.json
+0 −2,128 composer/libresign/pdf-signature-validator/vendor-bin/psalm/composer.lock
+0 −11 composer/libresign/pdf-signature-validator/vendor-bin/rector/composer.json
+0 −146 composer/libresign/pdf-signature-validator/vendor-bin/rector/composer.lock
+0 −1 composer/libresign/pdf-signature-validator/vendor-bin/update/composer.json
+5 −1 composer/mikehaertl/php-pdftk/src/Pdf.php
282 changes: 194 additions & 88 deletions lib/Handler/SignEngine/Pkcs12Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace OCA\Libresign\Handler\SignEngine;

use DateTime;
use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
Expand All @@ -18,21 +19,17 @@
use OCA\Libresign\Service\CaIdentifierService;
use OCA\Libresign\Service\Crl\CrlService;
use OCA\Libresign\Service\FolderService;
use OCA\Libresign\Service\Signature\PdfSignatureValidationService;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationReason;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor;
use OCA\Libresign\Vendor\phpseclib3\File\ASN1;
use OCP\Files\File;
use OCP\IAppConfig;
use OCP\IL10N;
use OCP\ITempManager;
use Psr\Log\LoggerInterface;

class Pkcs12Handler extends SignEngineHandler {
use OrderCertificatesTrait;
protected string $certificate = '';
private array $signaturesFromPoppler = [];
private ?JSignPdfHandler $jSignPdfHandler = null;
private string $rootCertificatePem = '';
private bool $isLibreSignFile = false;
Expand All @@ -43,12 +40,11 @@ public function __construct(
protected CertificateEngineFactory $certificateEngineFactory,
private IL10N $l10n,
private FooterHandler $footerHandler,
private ITempManager $tempManager,
private LoggerInterface $logger,
private CaIdentifierService $caIdentifierService,
private DocMdpHandler $docMdpHandler,
private CrlService $crlService,
private PdfSignatureValidationService $pdfSignatureValidationService,
private PdfSignatureExtractor $pdfSignatureExtractor,
) {
parent::__construct($l10n, $folderService, $logger);
}
Expand Down Expand Up @@ -96,45 +92,28 @@ public function setIsLibreSignFile(): void {
#[\Override]
public function getCertificateChain($resource): array {
$certificates = [];
$nativeMetadata = array_values($this->extractNativeSignatureMetadata($resource));
rewind($resource);
$nativeValidation = array_values($this->pdfSignatureValidationService->validateFromResource($resource));
$index = 0;

foreach ($this->getSignatures($resource) as $signature) {
$metadata = $nativeMetadata[$index] ?? [];
$validation = $nativeValidation[$index] ?? [];
$index++;

if (!$signature) {
continue;
}

$result = $this->processSignature(
$resource,
$signature,
$metadata,
$validation
);
$result = $this->processSignature($resource, $signature);

if (empty($result['chain'])) {
continue;
}

$certificates[] = $result;
}

return $certificates;
}

private function processSignature($resource, ?string $signature, array $metadata = [], array $validation = []): array {
private function processSignature($resource, ?string $signature): array {
$result = [];

if (!$signature) {
$result['chain'][0]['signature_validation'] = [
'id' => 3,
'label' => $this->l10n->t('Digest mismatch.'),
];
$result['chain'][0]['signature_validation'] = $this->getReadableSigState('Digest Mismatch.');
return $result;
}

Expand All @@ -144,7 +123,7 @@ private function processSignature($resource, ?string $signature, array $metadata
$chain = $this->extractCertificateChain($signature);
if (!empty($chain)) {
$result['chain'] = $this->orderCertificates($chain);
$result = $this->enrichLeafWithNativeData($result, $metadata, $validation);
$result = $this->enrichLeafWithPopplerData($resource, $result);
}

$result = $this->extractDocMdpData($resource, $result);
Expand Down Expand Up @@ -296,89 +275,216 @@ private function getRootCertificatePem(): string {
return $this->rootCertificatePem;
}

private function enrichLeafWithNativeData(array $result, array $metadata, array $validation): array {
private function enrichLeafWithPopplerData($resource, array $result): array {
if (empty($result['chain'])) {
return $result;
}

$leaf = &$result['chain'][0];

foreach (['field', 'range', 'signature_type', 'signing_hash_algorithm', 'covers_entire_document'] as $key) {
if (array_key_exists($key, $metadata)) {
$leaf[$key] = $metadata[$key];
}
$popplerOnlyFields = ['field', 'range', 'certificate_validation'];
if (!isset($result['chain'][0]['subject'])) {
return $result;
}

if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) {
$signatureValidation = $validation['signatureValidation'];

// Keep legacy OpenSSL result when native validator reports this known false-positive.
if (!$this->isDigestMismatchSignatureValidation($validation)) {
$leaf['signature_validation'] = $signatureValidation;
$needPoppler = false;
foreach ($popplerOnlyFields as $field) {
if (empty($result['chain'][0][$field])) {
$needPoppler = true;
break;
}
}

if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) {
$leaf['certificate_validation'] = $validation['certificateValidation'];
if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) {
$needPoppler = true;
}

if (!isset($leaf['certificate_validation'])) {
$leaf['certificate_validation'] = [
'id' => 3,
'label' => $this->l10n->t('Certificate issuer is unknown.'),
];
if (!$needPoppler) {
return $result;
}
$popplerChain = $this->chainFromPoppler($result['chain'][0]['subject'], $resource);
if (empty($popplerChain)) {
return $result;
}
foreach ($popplerOnlyFields as $field) {
if (isset($popplerChain[$field])) {
$result['chain'][0][$field] = $popplerChain[$field];
}
}
if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) {
if (isset($popplerChain['signature_validation'])) {
$result['chain'][0]['signature_validation'] = $popplerChain['signature_validation'];
}
}

return $result;
}

/**
* signer engines can produce signatures that the native validator currently flags as digest mismatch.
* In this case we preserve the legacy validation computed from the PKCS#7 signature.
*/
private function isDigestMismatchSignatureValidation(array $validation): bool {
$rawSignatureValidation = $validation['raw']['signature'] ?? null;
if ($rawSignatureValidation instanceof ValidationResult) {
return $rawSignatureValidation->reasonCode === ValidationReason::DIGEST_MISMATCH
|| $rawSignatureValidation->state === ValidationState::DIGEST_MISMATCH;
private function chainFromPoppler(array $subject, $resource): array {
$fromFallback = $this->popplerUtilsPdfSignFallback($resource);
foreach ($fromFallback as $popplerSig) {
if (!isset($popplerSig['chain'][0]['subject'])) {
continue;
}
if ($popplerSig['chain'][0]['subject'] == $subject) {
return $popplerSig['chain'][0];
}
}

$signatureValidation = $validation['signatureValidation'] ?? null;
return is_array($signatureValidation) && ($signatureValidation['id'] ?? null) === 3;
return [];
}

/**
* @param resource $resource
* @return array<int, array{field: ?string, range: ?array{offset1: int, offset2: int, length1: int, length2: int}, signature_type: ?string, covers_entire_document: bool}>
*/
private function extractNativeSignatureMetadata($resource): array {
private function popplerUtilsPdfSignFallback($resource): array {
if (!empty($this->signaturesFromPoppler)) {
return $this->signaturesFromPoppler;
}
if (shell_exec('which pdfsig') === null) {
return $this->signaturesFromPoppler;
}
rewind($resource);
$content = stream_get_contents($resource);
if (!is_string($content) || $content === '') {
return [];
}
$tempFile = $this->tempManager->getTemporaryFile('file.pdf');
file_put_contents($tempFile, $content);

try {
$signatures = $this->extractNativeSignaturesFromContent($content);
} catch (UnsignedPdfException) {
return [];
$content = shell_exec('env TZ=UTC pdfsig ' . $tempFile);
if (empty($content)) {
return $this->signaturesFromPoppler;
}
$metadata = [];
$lines = explode("\n", $content);

$lastSignature = 0;
foreach ($lines as $item) {
$isFirstLevel = preg_match('/^Signature\s#(\d)/', $item, $match);
if ($isFirstLevel) {
$lastSignature = (int)$match[1] - 1;
$this->signaturesFromPoppler[$lastSignature] = [];
continue;
}

foreach ($signatures as $index => $signature) {
$metadata[$index] = [
'field' => $signature->metadata->field,
'range' => $signature->metadata->range,
'signature_type' => $signature->metadata->signatureType,
'covers_entire_document' => $signature->metadata->coversEntireDocument,
];
$match = [];
$isSecondLevel = preg_match('/^\s+-\s(?<key>.+):\s(?<value>.*)/', $item, $match);
if ($isSecondLevel) {
switch ((string)$match['key']) {
case 'Signing Time':
$this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value'], new \DateTimeZone('UTC'));
break;
case 'Signer full Distinguished Name':
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['subject'] = $this->parseDistinguishedNameWithMultipleValues($match['value']);
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['name'] = $match['value'];
break;
case 'Signing Hash Algorithm':
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['signatureTypeSN'] = $match['value'];
break;
case 'Signature Validation':
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['signature_validation'] = $this->getReadableSigState($match['value']);
break;
case 'Certificate Validation':
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['certificate_validation'] = $this->getReadableCertState($match['value']);
break;
case 'Signed Ranges':
if (preg_match('/\[(\d+) - (\d+)\], \[(\d+) - (\d+)\]/', $match['value'], $ranges)) {
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['range'] = [
'offset1' => (int)$ranges[1],
'length1' => (int)$ranges[2],
'offset2' => (int)$ranges[3],
'length2' => (int)$ranges[4],
];
}
break;
case 'Signature Field Name':
$this->signaturesFromPoppler[$lastSignature]['chain'][0]['field'] = $match['value'];
break;
case 'Signature Validation':
case 'Signature Type':
case 'Total document signed':
case 'Not total document signed':
default:
break;
}
}
}
return $this->signaturesFromPoppler;
}

return $metadata;
private function getReadableSigState(string $status) {
return match ($status) {
'Signature is Valid.' => [
'id' => 1,
'label' => $this->l10n->t('Signature is valid.'),
],
'Signature is Invalid.' => [
'id' => 2,
'label' => $this->l10n->t('Signature is invalid.'),
],
'Digest Mismatch.' => [
'id' => 3,
'label' => $this->l10n->t('Digest mismatch.'),
],
"Document isn't signed or corrupted data." => [
'id' => 4,
'label' => $this->l10n->t("Document isn't signed or corrupted data."),
],
'Signature has not yet been verified.' => [
'id' => 5,
'label' => $this->l10n->t('Signature has not yet been verified.'),
],
default => [
'id' => 6,
'label' => $this->l10n->t('Unknown validation failure.'),
],
};
}

protected function extractNativeSignaturesFromContent(string $content): array {
return $this->pdfSignatureExtractor->extractFromString($content);
private function getReadableCertState(string $status) {
return match ($status) {
'Certificate is Trusted.' => [
'id' => 1,
'label' => $this->l10n->t('Certificate is trusted.'),
],
"Certificate issuer isn't Trusted." => [
'id' => 2,
'label' => $this->l10n->t("Certificate issuer isn't trusted."),
],
'Certificate issuer is unknown.' => [
'id' => 3,
'label' => $this->l10n->t('Certificate issuer is unknown.'),
],
'Certificate has been Revoked.' => [
'id' => 4,
'label' => $this->l10n->t('Certificate has been revoked.'),
],
'Certificate has Expired' => [
'id' => 5,
'label' => $this->l10n->t('Certificate has expired'),
],
'Certificate has not yet been verified.' => [
'id' => 6,
'label' => $this->l10n->t('Certificate has not yet been verified.'),
],
default => [
'id' => 7,
'label' => $this->l10n->t('Unknown issue with Certificate or corrupted data.')
],
};
}

private function parseDistinguishedNameWithMultipleValues(string $dn): array {
$result = [];
$pairs = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $dn);

foreach ($pairs as $pair) {
[$key, $value] = explode('=', $pair, 2);
if (empty($key) || empty($value)) {
return $result;
}
$key = trim($key);
$value = trim($value);
$value = trim($value, '"');

if (!isset($result[$key])) {
$result[$key] = $value;
} else {
if (!is_array($result[$key])) {
$result[$key] = [$result[$key]];
}
$result[$key][] = $value;
}
}

return $result;
}

private function der2pem($derData) {
Expand Down
Loading
Loading