Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
de47f04
refactor(validation): expose PDF validator package data
vitormattos Sep 4, 2026
31d1ab0
refactor(signing): remove legacy PDF signature parsing
vitormattos Sep 4, 2026
6869ec9
feat(validation): expose PDF modification state on signers
vitormattos Sep 4, 2026
e7acfe0
feat(api): add PDF modification state to signer response
vitormattos Sep 4, 2026
c3479bf
feat(validation): show PDF modification states
vitormattos Sep 4, 2026
14a7792
test(validation): cover structured PDF validation reasons
vitormattos Sep 4, 2026
c945862
test(validation): cover PDF modification state propagation
vitormattos Sep 4, 2026
088a21c
test(signing): update PDF validation integration tests
vitormattos Sep 4, 2026
7d7955f
test(validation): cover PDF modification warnings
vitormattos Sep 4, 2026
cd8a604
chore(openapi): regenerate signer validation models
vitormattos Sep 4, 2026
59281fa
test(signing): expect package certificate validation state
vitormattos Sep 4, 2026
a1a2b9a
fix(validation): expose PDF modification helpers
vitormattos Sep 4, 2026
8b2d5b5
fix(validation): update PDF validator result types
vitormattos Sep 4, 2026
faa7ea0
fix(validation): preserve PDF modification state types
vitormattos Sep 4, 2026
6c12568
fix(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
4281751
fix(validation): isolate scoped vendor result types
vitormattos Sep 4, 2026
775b7dc
refactor(validation): use validator result types directly
vitormattos Sep 4, 2026
36a606a
test(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
7ceea7e
test(validation): cover complete validator results
vitormattos Sep 4, 2026
23b0002
test(validation): cover certificate display name priority
vitormattos Sep 4, 2026
a04af37
test(validation): cover remaining mutation cases
vitormattos Sep 4, 2026
7806aaa
fix(validation): wrap long detail text
vitormattos Sep 4, 2026
15d05f5
fix(validation): accept external PDF payloads
vitormattos Sep 4, 2026
a9a8c93
test(validation): cover external PDF payloads
vitormattos Sep 4, 2026
542da4d
fix(validation): handle upload validation errors
vitormattos Sep 4, 2026
01bd471
test(validation): cover modification status presentation
vitormattos Sep 4, 2026
138f6b1
fix(validation): wrap long validation details
vitormattos Sep 4, 2026
cd1b68f
fix(validation): prevent detail text truncation
vitormattos Sep 4, 2026
9c40578
fix(validation): wrap certification description
vitormattos Sep 4, 2026
2d8fe14
fix(validation): summarize document validation state
vitormattos Sep 4, 2026
01c2573
fix(validation): render document summary severity
vitormattos Sep 4, 2026
5640e21
fix(validation): render envelope summary severity
vitormattos Sep 4, 2026
c316dfb
test(validation): preserve modification state literals
vitormattos Sep 4, 2026
8447249
fix(validation): type validation signer metadata
vitormattos Sep 4, 2026
2b21410
refactor(validation): simplify detail styles
vitormattos Sep 4, 2026
75b3bf6
test(validation): cover document summary states
vitormattos Sep 4, 2026
1543759
fix(validation): validate modification state
vitormattos Sep 4, 2026
04fb128
test(validation): cover document summary states
vitormattos Sep 4, 2026
adeec26
test(validation): cover signer severity
vitormattos Sep 4, 2026
e83db72
refactor(types): define PDF validation result contracts
vitormattos Sep 4, 2026
3d2d145
refactor(validation): reuse PDF validation result types
vitormattos Sep 4, 2026
a426a93
fix(validation): reflect signer validation severity
vitormattos Sep 4, 2026
857e97e
fix(validation): ignore missing validation results
vitormattos Sep 4, 2026
176dbab
test(validation): cover missing validation results
vitormattos Sep 4, 2026
84b9823
fix(validation): size signer status icon
vitormattos Sep 4, 2026
54f0314
fix(openapi): keep internal validator types out of responses
vitormattos Sep 4, 2026
32b96b5
refactor(validation): define internal result types locally
vitormattos Sep 4, 2026
1c43b7d
refactor(validation): simplify validation messages and types
vitormattos Sep 4, 2026
9d8672f
style(types): remove extra blank line
vitormattos Sep 4, 2026
def78e6
refactor(validation): reuse mapped result type
vitormattos Sep 4, 2026
6809a38
refactor(validation): move internal result types
vitormattos Sep 4, 2026
99b86ef
docs(l10n): add context for signature validation
vitormattos Sep 4, 2026
4410e0f
fix(validation): restore signer icon size and remove ghost background
vitormattos Sep 4, 2026
2dfe06e
fix(validation): use plain glyphs for signer status badges
vitormattos Sep 4, 2026
56508d0
fix(validation): restore required status icons
vitormattos Sep 4, 2026
d0ef5a7
fix(validation): use list item icon slot as intended
vitormattos Sep 4, 2026
8210ad8
test(validation): update structured reason expectation
vitormattos Sep 4, 2026
07073f1
fix(validation): improve signer status icon contrast
vitormattos Sep 4, 2026
c00179f
fix(validation): clarify certification status icon
vitormattos Sep 4, 2026
3129b3d
test(validation): update status class expectations
vitormattos Sep 4, 2026
0d9df92
fix(validation): ignore expected signature revisions
vitormattos Sep 4, 2026
65702ce
fix(validation): harden PDF validation presentation
vitormattos Sep 4, 2026
d8e95c1
refactor(validation): use validator result contract
vitormattos Sep 4, 2026
aad7863
fix(validation): map structural validation reasons
vitormattos Sep 5, 2026
5481c81
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
acc36f7
fix(validation): remove duplicated validation reasons
vitormattos Sep 5, 2026
7b024af
refactor(validation): remove unused icon
vitormattos Sep 5, 2026
7577953
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
54b8162
fix: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
c3bfe04
test: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
7195f69
style: fix JSignPdfHandler formatting
vitormattos Sep 5, 2026
9a19ac3
fix: preserve signatures without binary payload
vitormattos Sep 5, 2026
0c7c205
test: cover signatures without binary payload
vitormattos Sep 5, 2026
81fc906
fix: keep stable35 JSignPdf handler
vitormattos Sep 6, 2026
f3f3a6c
test: keep stable35 JSignPdf expectations
vitormattos Sep 6, 2026
08bbc56
chore: update pdf signature validator dependency
vitormattos Sep 6, 2026
4b029c7
chore: update 3rdparty
vitormattos Sep 6, 2026
2eedc51
fix: restore stable35 third-party dependencies
vitormattos Sep 6, 2026
45dc4a2
fix: import structured validation reasons
vitormattos Sep 6, 2026
b413e43
fix: import timestamp validation model
vitormattos Sep 6, 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