Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a7ca2e1
chore: update pdf-signature-validator
vitormattos Sep 4, 2026
4cd63b5
refactor(validation): expose PDF validator package data
vitormattos Sep 4, 2026
c1a18c9
refactor(signing): remove legacy PDF signature parsing
vitormattos Sep 4, 2026
b2b200d
feat(validation): expose PDF modification state on signers
vitormattos Sep 4, 2026
279bacc
feat(api): add PDF modification state to signer response
vitormattos Sep 4, 2026
69f93b7
feat(validation): show PDF modification states
vitormattos Sep 4, 2026
583b56b
test(validation): cover structured PDF validation reasons
vitormattos Sep 4, 2026
db208e9
test(validation): cover PDF modification state propagation
vitormattos Sep 4, 2026
a5bafcd
test(signing): update PDF validation integration tests
vitormattos Sep 4, 2026
e683a6f
test(validation): cover PDF modification warnings
vitormattos Sep 4, 2026
a33ed38
chore(openapi): regenerate signer validation models
vitormattos Sep 4, 2026
19bd2d6
test(signing): expect package certificate validation state
vitormattos Sep 4, 2026
5942a17
fix(validation): expose PDF modification helpers
vitormattos Sep 4, 2026
eeb2dfe
fix(validation): update PDF validator result types
vitormattos Sep 4, 2026
f56c6f7
fix(validation): preserve PDF modification state types
vitormattos Sep 4, 2026
633d5f7
fix(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
44dae65
fix(validation): isolate scoped vendor result types
vitormattos Sep 4, 2026
39d4888
chore: update pdf-signature-validator to 0.5.1
vitormattos Sep 4, 2026
74b2823
refactor(validation): use validator result types directly
vitormattos Sep 4, 2026
9273563
test(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
b55d2d0
test(validation): cover complete validator results
vitormattos Sep 4, 2026
bd7e236
test(validation): cover certificate display name priority
vitormattos Sep 4, 2026
2585b68
test(validation): cover remaining mutation cases
vitormattos Sep 4, 2026
9bee55d
fix(validation): wrap long detail text
vitormattos Sep 4, 2026
86f819d
fix(validation): accept external PDF payloads
vitormattos Sep 4, 2026
2cbcb03
test(validation): cover external PDF payloads
vitormattos Sep 4, 2026
331a19c
fix(validation): handle upload validation errors
vitormattos Sep 4, 2026
98872b7
test(validation): cover modification status presentation
vitormattos Sep 4, 2026
b0af6d0
fix(validation): wrap long validation details
vitormattos Sep 4, 2026
7e71955
fix(validation): prevent detail text truncation
vitormattos Sep 4, 2026
8b9b871
fix(validation): wrap certification description
vitormattos Sep 4, 2026
82e9e84
fix(validation): summarize document validation state
vitormattos Sep 4, 2026
fca79a4
fix(validation): render document summary severity
vitormattos Sep 4, 2026
3284d79
fix(validation): render envelope summary severity
vitormattos Sep 4, 2026
488c576
test(validation): preserve modification state literals
vitormattos Sep 4, 2026
780155c
fix(validation): type validation signer metadata
vitormattos Sep 4, 2026
ccbdeeb
refactor(validation): simplify detail styles
vitormattos Sep 4, 2026
03e8a6d
test(validation): cover document summary states
vitormattos Sep 4, 2026
4aad654
fix(validation): validate modification state
vitormattos Sep 4, 2026
28c7d5e
test(validation): cover document summary states
vitormattos Sep 4, 2026
d35b046
test(validation): cover signer severity
vitormattos Sep 4, 2026
e6746e3
refactor(types): define PDF validation result contracts
vitormattos Sep 4, 2026
1bd988a
refactor(validation): reuse PDF validation result types
vitormattos Sep 4, 2026
e161ae1
fix(validation): reflect signer validation severity
vitormattos Sep 4, 2026
f2ce926
fix(validation): ignore missing validation results
vitormattos Sep 4, 2026
22f5375
test(validation): cover missing validation results
vitormattos Sep 4, 2026
57e6425
fix(validation): size signer status icon
vitormattos Sep 4, 2026
8090110
fix(openapi): keep internal validator types out of responses
vitormattos Sep 4, 2026
04ecb81
refactor(validation): define internal result types locally
vitormattos Sep 4, 2026
db37fd7
refactor(validation): simplify validation messages and types
vitormattos Sep 4, 2026
3dfc7a6
style(types): remove extra blank line
vitormattos Sep 4, 2026
de9f10e
refactor(validation): reuse mapped result type
vitormattos Sep 4, 2026
4d117a5
refactor(validation): move internal result types
vitormattos Sep 4, 2026
52407b1
docs(l10n): add context for signature validation
vitormattos Sep 4, 2026
659b638
fix(validation): restore signer icon size and remove ghost background
vitormattos Sep 4, 2026
fa8f7e9
fix(validation): use plain glyphs for signer status badges
vitormattos Sep 4, 2026
dbecdbb
fix(validation): restore required status icons
vitormattos Sep 4, 2026
8ed7adb
fix(validation): use list item icon slot as intended
vitormattos Sep 4, 2026
b82db2a
test(validation): update structured reason expectation
vitormattos Sep 4, 2026
498d482
fix(validation): improve signer status icon contrast
vitormattos Sep 4, 2026
2c60335
fix(validation): clarify certification status icon
vitormattos Sep 4, 2026
1ab7b46
test(validation): update status class expectations
vitormattos Sep 4, 2026
1fe4e96
fix(validation): ignore expected signature revisions
vitormattos Sep 4, 2026
211f2e7
fix(validation): harden PDF validation presentation
vitormattos Sep 4, 2026
1c617cc
refactor(validation): use validator result contract
vitormattos Sep 4, 2026
6c394fa
chore: update pdf signature validator to 0.5.2
vitormattos Sep 5, 2026
0eab8c9
fix(validation): map structural validation reasons
vitormattos Sep 5, 2026
7c16f89
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
db53e9c
fix(validation): remove duplicated validation reasons
vitormattos Sep 5, 2026
0b25a79
refactor(validation): remove unused icon
vitormattos Sep 5, 2026
206fcc3
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
4dd3afa
fix: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
cc1f3b5
test: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
fdb04dc
style: fix JSignPdfHandler formatting
vitormattos Sep 5, 2026
eb9e42a
fix: preserve signatures without binary payload
vitormattos Sep 5, 2026
aaba85e
test: cover signatures without binary payload
vitormattos Sep 5, 2026
20d898c
merge: update branch after JSignPdf 3.1.0
vitormattos Sep 5, 2026
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
254 changes: 117 additions & 137 deletions lib/Handler/SignEngine/Pkcs12Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
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\Parser\PdfSignatureExtractor;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ExtractedSignature;
use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\TimestampToken;
use OCA\Libresign\Vendor\phpseclib4\Exception\UnexpectedValueException;
use OCA\Libresign\Vendor\phpseclib4\File\ASN1;
use OCP\Files\File;
Expand Down Expand Up @@ -47,7 +47,6 @@ public function __construct(
private DocMdpHandler $docMdpHandler,
private CrlService $crlService,
private PdfSignatureValidationService $pdfSignatureValidationService,
private PdfSignatureExtractor $pdfSignatureExtractor,
) {
parent::__construct($l10n, $folderService, $logger);
}
Expand All @@ -57,38 +56,6 @@ protected function getCertificateEngineFactory(): CertificateEngineFactory {
return $this->certificateEngineFactory;
}

/**
* @throws LibresignException When is not a signed file
*/
private function getSignatures($resource): iterable {
rewind($resource);
$content = stream_get_contents($resource);

preg_match_all('/\/Contents\s*<([0-9a-fA-F]+)>/', $content, $contents, PREG_OFFSET_CAPTURE);

if (empty($contents[1])) {
// TRANSLATORS Error while LibreSign reads a PDF for signature validation: the file has no embedded PKCS#12/PDF signature bytes yet.
throw new LibresignException($this->l10n->t('Unsigned file.'));
}

$seenHexSignatures = [];
foreach ($contents[1] as $match) {
$signatureHex = $match[0];

if (isset($seenHexSignatures[$signatureHex])) {
continue;
}
$seenHexSignatures[$signatureHex] = true;

$decodedSignature = @hex2bin($signatureHex);
if ($decodedSignature === false) {
yield null;
continue;
}
yield $decodedSignature;
}
}

public function setIsLibreSignFile(): void {
$this->isLibreSignFile = true;
}
Expand All @@ -113,25 +80,25 @@ public function getCertificateChain($resource): array {
$certificateEngine->setPolicyUserIdForValidation($this->policyUserIdForValidation);

try {
$nativeMetadata = array_values($this->extractNativeSignatureMetadata($resource));
rewind($resource);
$nativeValidation = array_values($this->pdfSignatureValidationService->validateFromResource($resource));
$index = 0;
$validationResults = array_values(
$this->pdfSignatureValidationService->validateFromResource($resource)
);

foreach ($this->getSignatures($resource) as $signature) {
$metadata = $nativeMetadata[$index] ?? [];
$validation = $nativeValidation[$index] ?? [];
$index++;
if ($validationResults === []) {
throw new LibresignException($this->l10n->t('Unsigned file.'));
}

if (!$signature) {
foreach ($validationResults as $validation) {
$signature = $validation['signature'] ?? null;
if (!$signature instanceof ExtractedSignature) {
continue;
}

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

if (empty($result['chain'])) {
Expand All @@ -148,35 +115,53 @@ public function getCertificateChain($resource): array {
return $certificates;
}

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

if (!$signature) {
$result['chain'][0]['signature_validation'] = [
'id' => 3,
// TRANSLATORS Status label on LibreSign's public/document validation UI when the PDF signature hash does not match the document bytes (tamper or corrupt signature).
'label' => $this->l10n->t('Digest mismatch.'),
];
return $result;
private function processSignature(
$resource,
ExtractedSignature $signature,
array $validation = [],
): array {
$binarySignature = $signature->binarySignature;
if ($binarySignature === null || $binarySignature === '') {
return $this->enrichLeafWithNativeData(
['chain' => [[]]],
$signature,
$validation,
);
}

$result = [];

try {
$decoded = ASN1::decodeBER($signature);
$decoded = ASN1::decodeBER($binarySignature);
} catch (UnexpectedValueException) {
return [];
$decoded = null;
}

$result = $this->extractSigningTime($decoded, $result);

$timestamp = $validation['timestamp'] ?? null;
if ($timestamp instanceof TimestampToken) {
$result['timestamp'] = $this->mapTimestampToken($timestamp);
}
$result = $this->extractTimestampData($decoded, $result);

$chain = $this->extractCertificateChain($signature);
$pemCertificates = $validation['certificates'] ?? [];
if (!is_array($pemCertificates)) {
$pemCertificates = [];
}

$chain = $this->extractCertificateChain($pemCertificates);
if (!empty($chain)) {
$result['chain'] = $this->orderCertificates($chain);
$result = $this->enrichLeafWithNativeData($result, $metadata, $validation);
$result = $this->enrichLeafWithNativeData(
$result,
$signature,
$validation,
);
}

$result = $this->extractDocMdpData($resource, $result);

$result = $this->applyLibreSignRootCAFlag($result);
return $result;
return $this->applyLibreSignRootCAFlag($result);
}

private function applyLibreSignRootCAFlag(array $signer): array {
Expand Down Expand Up @@ -209,55 +194,71 @@ private function extractDocMdpData($resource, array $result): array {
return array_merge($result, $docMdpData);
}

private function extractTimestampData(?array $decoded, array $result): array {
private function extractSigningTime(?array $decoded, array $result): array {
if ($decoded === null) {
return $result;
}

$tsa = new TSA();

$timestampData = $tsa->extract($decoded);
if (!empty($timestampData['genTime']) || !empty($timestampData['policy']) || !empty($timestampData['serialNumber'])) {
$result['timestamp'] = $timestampData;
$signingTime = $tsa->getSigninTime($decoded);
if ($signingTime instanceof \DateTime) {
$result['signingTime'] = $signingTime;
}

if (!isset($result['signingTime']) || !$result['signingTime'] instanceof \DateTime) {
$result['signingTime'] = $tsa->getSigninTime($decoded);
}
return $result;
}

private function extractCertificateChain(string $signature): array {
$pkcs7PemSignature = $this->der2pem($signature);
$pemCertificates = [];

if (!openssl_pkcs7_read($pkcs7PemSignature, $pemCertificates)) {
return [];
}
private function mapTimestampToken(TimestampToken $timestamp): array {
$result = [
'genTime' => $timestamp->generatedAt,
'policy' => $timestamp->policyOid,
'serialNumber' => $timestamp->serialNumber,
'cnHints' => $timestamp->certificateSubject,
'tsaName' => $timestamp->certificateSubject['commonName'] ?? null,
];

return array_filter(
$result,
static fn (mixed $value): bool => $value !== null
&& $value !== ''
&& $value !== [],
);
}

/**
* @param list<string> $pemCertificates
*/
private function extractCertificateChain(array $pemCertificates): array {
$chain = [];
$isLibreSignRootCA = false;
$certificateEngine = $this->getCertificateEngine();

foreach ($pemCertificates as $index => $pemCertificate) {
if (!is_string($pemCertificate) || $pemCertificate === '') {
continue;
}

$parsed = $certificateEngine->parseCertificate($pemCertificate);
if ($parsed) {
$parsed['signature_validation'] = [
'id' => 1,
// TRANSLATORS Status label on LibreSign signature validation when the cryptographic PDF signature checks out successfully.
'label' => $this->l10n->t('Signature is valid.'),
];
if (!$isLibreSignRootCA) {
$isLibreSignRootCA = $this->isLibreSignRootCA($pemCertificate, $parsed);
}
$parsed['isLibreSignRootCA'] = $isLibreSignRootCA;
$chain[$index] = $parsed;
if (!$parsed) {
continue;
}

if (!$isLibreSignRootCA) {
$isLibreSignRootCA = $this->isLibreSignRootCA(
$pemCertificate,
$parsed,
);
}

$parsed['isLibreSignRootCA'] = $isLibreSignRootCA;
$chain[$index] = $parsed;
}

if ($isLibreSignRootCA || $this->isLibreSignFile) {
foreach ($chain as &$cert) {
$cert['isLibreSignRootCA'] = true;
}
unset($cert);
}

return $chain;
Expand Down Expand Up @@ -310,38 +311,57 @@ private function getRootCertificatePem(): string {
return $this->rootCertificatePem;
}
$configPath = $this->appConfig->getValueString(Application::APP_ID, 'config_path');
$caPemPath = $configPath . DIRECTORY_SEPARATOR . 'ca.pem';

if (empty($configPath)
|| !is_dir($configPath)
|| !is_readable($configPath . DIRECTORY_SEPARATOR . 'ca.pem')
|| !is_readable($caPemPath)
) {
return '';
}
$rootCertificatePem = file_get_contents($configPath . DIRECTORY_SEPARATOR . 'ca.pem');

$rootCertificatePem = file_get_contents($caPemPath);
if ($rootCertificatePem === false) {
return '';
}
$this->rootCertificatePem = $rootCertificatePem;
return $this->rootCertificatePem;
}

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

$leaf = &$result['chain'][0];
$metadata = $signature->metadata;

foreach (['field', 'range', 'signature_type', 'signing_hash_algorithm', 'covers_entire_document'] as $key) {
if (array_key_exists($key, $metadata)) {
$leaf[$key] = $metadata[$key];
}
$leaf['field'] = $metadata->field;
$leaf['range'] = $metadata->range;
$leaf['signature_type'] = $metadata->signatureType;
$leaf['signing_hash_algorithm'] = $signature->hashAlgorithm;
$leaf['covers_entire_document'] = $metadata->coversEntireDocument;

if ($metadata->documentModificationState !== null) {
$leaf['document_modification_state']
= $metadata->documentModificationState->value;
}

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

if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) {
if (
isset($validation['certificateValidation'])
&& is_array($validation['certificateValidation'])
) {
$leaf['certificate_validation'] = $validation['certificateValidation'];
}

Expand All @@ -356,46 +376,6 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array
return $result;
}

/**
* @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 {
rewind($resource);
$content = stream_get_contents($resource);
if (!is_string($content) || $content === '') {
return [];
}

try {
$signatures = $this->extractNativeSignaturesFromContent($content);
} catch (UnsignedPdfException) {
return [];
}
$metadata = [];

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,
];
}

return $metadata;
}

protected function extractNativeSignaturesFromContent(string $content): array {
return $this->pdfSignatureExtractor->extractFromString($content);
}

private function der2pem($derData) {
$pem = chunk_split(base64_encode((string)$derData), 64, "\n");
$pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
return $pem;
}

private function getHandler(): SignEngineHandler {
$sign_engine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'JSignPdf');
$property = lcfirst($sign_engine) . 'Handler';
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
* sign_request_uuid?: string,
* hash_algorithm?: string,
* covers_entire_document?: bool,
* document_modification_state?: 'unchanged'|'unsigned_content'|'trailing_data'|'invalid_byte_range'|'invalid_eof_boundary',
* me: bool,
* signingOrder?: non-negative-int,
* visibleElements: LibresignVisibleElement[],
Expand Down
Loading
Loading