Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
3177706
refactor(validation): expose PDF validator package data
vitormattos Sep 4, 2026
8796a04
refactor(signing): remove legacy PDF signature parsing
vitormattos Sep 4, 2026
26b0152
feat(validation): expose PDF modification state on signers
vitormattos Sep 4, 2026
3e3b0a6
feat(api): add PDF modification state to signer response
vitormattos Sep 4, 2026
c6e7f6e
feat(validation): show PDF modification states
vitormattos Sep 4, 2026
1fdbccd
test(validation): cover structured PDF validation reasons
vitormattos Sep 4, 2026
d5947dc
test(validation): cover PDF modification state propagation
vitormattos Sep 4, 2026
fb1094f
test(signing): update PDF validation integration tests
vitormattos Sep 4, 2026
f6b8293
test(validation): cover PDF modification warnings
vitormattos Sep 4, 2026
4984fbe
chore(openapi): regenerate signer validation models
vitormattos Sep 4, 2026
d0f5582
test(signing): expect package certificate validation state
vitormattos Sep 4, 2026
a6cd4a8
fix(validation): expose PDF modification helpers
vitormattos Sep 4, 2026
69d8201
fix(validation): update PDF validator result types
vitormattos Sep 4, 2026
8dbac50
fix(validation): preserve PDF modification state types
vitormattos Sep 4, 2026
3391657
fix(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
4ae38d0
fix(validation): isolate scoped vendor result types
vitormattos Sep 4, 2026
1946e69
refactor(validation): use validator result types directly
vitormattos Sep 4, 2026
fc6c24d
test(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
f9f16f9
test(validation): cover complete validator results
vitormattos Sep 4, 2026
baf94ee
test(validation): cover certificate display name priority
vitormattos Sep 4, 2026
e1510be
test(validation): cover remaining mutation cases
vitormattos Sep 4, 2026
9bef52d
fix(validation): wrap long detail text
vitormattos Sep 4, 2026
28dd67f
fix(validation): accept external PDF payloads
vitormattos Sep 4, 2026
151dfb3
test(validation): cover external PDF payloads
vitormattos Sep 4, 2026
d8982af
fix(validation): handle upload validation errors
vitormattos Sep 4, 2026
fb0c776
test(validation): cover modification status presentation
vitormattos Sep 4, 2026
28a294b
fix(validation): wrap long validation details
vitormattos Sep 4, 2026
5270d39
fix(validation): prevent detail text truncation
vitormattos Sep 4, 2026
8cdeb83
fix(validation): wrap certification description
vitormattos Sep 4, 2026
fe9a4df
fix(validation): summarize document validation state
vitormattos Sep 4, 2026
1417fa4
fix(validation): render document summary severity
vitormattos Sep 4, 2026
afb1854
fix(validation): render envelope summary severity
vitormattos Sep 4, 2026
842cb18
test(validation): preserve modification state literals
vitormattos Sep 4, 2026
f23f923
fix(validation): type validation signer metadata
vitormattos Sep 4, 2026
dd9b03c
refactor(validation): simplify detail styles
vitormattos Sep 4, 2026
6c7dc58
test(validation): cover document summary states
vitormattos Sep 4, 2026
9447800
fix(validation): validate modification state
vitormattos Sep 4, 2026
2b4d07a
test(validation): cover document summary states
vitormattos Sep 4, 2026
35dd9d0
test(validation): cover signer severity
vitormattos Sep 4, 2026
a433a5b
refactor(types): define PDF validation result contracts
vitormattos Sep 4, 2026
f8ba736
refactor(validation): reuse PDF validation result types
vitormattos Sep 4, 2026
f70e0bc
fix(validation): reflect signer validation severity
vitormattos Sep 4, 2026
722f4b9
fix(validation): ignore missing validation results
vitormattos Sep 4, 2026
da952b5
test(validation): cover missing validation results
vitormattos Sep 4, 2026
395a16c
fix(validation): size signer status icon
vitormattos Sep 4, 2026
e1b95f7
fix(openapi): keep internal validator types out of responses
vitormattos Sep 4, 2026
33cd18b
refactor(validation): define internal result types locally
vitormattos Sep 4, 2026
b8efb67
refactor(validation): simplify validation messages and types
vitormattos Sep 4, 2026
4602ec8
style(types): remove extra blank line
vitormattos Sep 4, 2026
2f7c1ae
refactor(validation): reuse mapped result type
vitormattos Sep 4, 2026
1dc62e3
refactor(validation): move internal result types
vitormattos Sep 4, 2026
1327634
docs(l10n): add context for signature validation
vitormattos Sep 4, 2026
f855477
fix(validation): restore signer icon size and remove ghost background
vitormattos Sep 4, 2026
21f1fbf
fix(validation): use plain glyphs for signer status badges
vitormattos Sep 4, 2026
13e7e97
fix(validation): restore required status icons
vitormattos Sep 4, 2026
27c8593
fix(validation): use list item icon slot as intended
vitormattos Sep 4, 2026
e2e81e4
test(validation): update structured reason expectation
vitormattos Sep 4, 2026
837db2f
fix(validation): improve signer status icon contrast
vitormattos Sep 4, 2026
c4a8a3a
fix(validation): clarify certification status icon
vitormattos Sep 4, 2026
463e4a3
test(validation): update status class expectations
vitormattos Sep 4, 2026
ad92e13
fix(validation): ignore expected signature revisions
vitormattos Sep 4, 2026
fb1b705
fix(validation): harden PDF validation presentation
vitormattos Sep 4, 2026
6857a83
refactor(validation): use validator result contract
vitormattos Sep 4, 2026
cf63c48
fix(validation): map structural validation reasons
vitormattos Sep 5, 2026
b48f1df
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
0763fe0
fix(validation): remove duplicated validation reasons
vitormattos Sep 5, 2026
511cc1d
refactor(validation): remove unused icon
vitormattos Sep 5, 2026
eacbf67
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
8ef72d2
fix: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
bc6f631
test: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
90949ef
style: fix JSignPdfHandler formatting
vitormattos Sep 5, 2026
1c55fea
fix: preserve signatures without binary payload
vitormattos Sep 5, 2026
0389b52
test: cover signatures without binary payload
vitormattos Sep 5, 2026
318b7de
fix: keep stable33 JSignPdf handler
vitormattos Sep 6, 2026
2e3be5c
test: keep stable33 JSignPdf expectations
vitormattos Sep 6, 2026
f7bc4af
fix: use native validation results for certificate chain
vitormattos Sep 6, 2026
0a30dc2
fix: import structured validation reasons
vitormattos Sep 6, 2026
e53bf92
fix: backport envelope validation note type
vitormattos Sep 6, 2026
1ead6c0
fix: backport CRL validation icon classes
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
255 changes: 118 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 All @@ -46,43 +46,10 @@ public function __construct(
private DocMdpHandler $docMdpHandler,
private CrlService $crlService,
private PdfSignatureValidationService $pdfSignatureValidationService,
private PdfSignatureExtractor $pdfSignatureExtractor,
) {
parent::__construct($l10n, $folderService, $logger);
}

/**
* @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 @@ -95,25 +62,26 @@ 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;
$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 @@ -126,35 +94,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->extractTimestampData($decoded, $result);

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

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

$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 @@ -187,55 +173,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 @@ -288,38 +290,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 @@ -334,46 +355,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 @@ -178,6 +178,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,
* status: 0|1|2,
* signingOrder?: non-negative-int,
Expand Down
8 changes: 8 additions & 0 deletions lib/Service/File/CertificateSignersMergeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,14 @@ private function enrichSignerWithCertificateValidation(\stdClass $signer, array
if (isset($endEntityCert['covers_entire_document']) && !isset($signer->covers_entire_document)) {
$signer->covers_entire_document = $endEntityCert['covers_entire_document'];
}

if (
isset($endEntityCert['document_modification_state'])
&& !isset($signer->document_modification_state)
) {
$signer->document_modification_state
= $endEntityCert['document_modification_state'];
}
}

/**
Expand Down
Loading
Loading