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
199e2c6
refactor(validation): expose PDF validator package data
vitormattos Sep 4, 2026
5ac22db
refactor(signing): remove legacy PDF signature parsing
vitormattos Sep 4, 2026
c8a5da0
feat(validation): expose PDF modification state on signers
vitormattos Sep 4, 2026
d2546d2
feat(api): add PDF modification state to signer response
vitormattos Sep 4, 2026
64d524b
feat(validation): show PDF modification states
vitormattos Sep 4, 2026
1fbec83
test(validation): cover structured PDF validation reasons
vitormattos Sep 4, 2026
6517f24
test(validation): cover PDF modification state propagation
vitormattos Sep 4, 2026
16225dc
test(signing): update PDF validation integration tests
vitormattos Sep 4, 2026
b2cdf07
test(validation): cover PDF modification warnings
vitormattos Sep 4, 2026
2aade0d
chore(openapi): regenerate signer validation models
vitormattos Sep 4, 2026
2197bd6
test(signing): expect package certificate validation state
vitormattos Sep 4, 2026
d5c1432
fix(validation): expose PDF modification helpers
vitormattos Sep 4, 2026
1ecc4c7
fix(validation): update PDF validator result types
vitormattos Sep 4, 2026
5d01d06
fix(validation): preserve PDF modification state types
vitormattos Sep 4, 2026
65e83da
fix(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
281a1d6
fix(validation): isolate scoped vendor result types
vitormattos Sep 4, 2026
679ab1a
refactor(validation): use validator result types directly
vitormattos Sep 4, 2026
c7a4d41
test(validation): preserve TSA certificate hints
vitormattos Sep 4, 2026
cdbd251
test(validation): cover complete validator results
vitormattos Sep 4, 2026
239bc57
test(validation): cover certificate display name priority
vitormattos Sep 4, 2026
7187a77
test(validation): cover remaining mutation cases
vitormattos Sep 4, 2026
d671f2c
fix(validation): wrap long detail text
vitormattos Sep 4, 2026
afc0e80
fix(validation): accept external PDF payloads
vitormattos Sep 4, 2026
b19499d
test(validation): cover external PDF payloads
vitormattos Sep 4, 2026
018c690
fix(validation): handle upload validation errors
vitormattos Sep 4, 2026
d6e0fe4
test(validation): cover modification status presentation
vitormattos Sep 4, 2026
e8e21a6
fix(validation): wrap long validation details
vitormattos Sep 4, 2026
d7ad8d3
fix(validation): prevent detail text truncation
vitormattos Sep 4, 2026
b8236f5
fix(validation): wrap certification description
vitormattos Sep 4, 2026
73c7e44
fix(validation): summarize document validation state
vitormattos Sep 4, 2026
1927522
fix(validation): render document summary severity
vitormattos Sep 4, 2026
2211a26
fix(validation): render envelope summary severity
vitormattos Sep 4, 2026
96fe889
test(validation): preserve modification state literals
vitormattos Sep 4, 2026
56482bc
fix(validation): type validation signer metadata
vitormattos Sep 4, 2026
fc57b44
refactor(validation): simplify detail styles
vitormattos Sep 4, 2026
e1ae86f
test(validation): cover document summary states
vitormattos Sep 4, 2026
c3f956d
fix(validation): validate modification state
vitormattos Sep 4, 2026
d86c5a2
test(validation): cover document summary states
vitormattos Sep 4, 2026
ef9043e
test(validation): cover signer severity
vitormattos Sep 4, 2026
afeadfe
refactor(types): define PDF validation result contracts
vitormattos Sep 4, 2026
528b157
refactor(validation): reuse PDF validation result types
vitormattos Sep 4, 2026
9d76526
fix(validation): reflect signer validation severity
vitormattos Sep 4, 2026
832d57a
fix(validation): ignore missing validation results
vitormattos Sep 4, 2026
6714306
test(validation): cover missing validation results
vitormattos Sep 4, 2026
0f0cd66
fix(validation): size signer status icon
vitormattos Sep 4, 2026
eff798a
fix(openapi): keep internal validator types out of responses
vitormattos Sep 4, 2026
b35852f
refactor(validation): define internal result types locally
vitormattos Sep 4, 2026
7949ca1
refactor(validation): simplify validation messages and types
vitormattos Sep 4, 2026
96e21bf
style(types): remove extra blank line
vitormattos Sep 4, 2026
8fa2414
refactor(validation): reuse mapped result type
vitormattos Sep 4, 2026
ffcf4e4
refactor(validation): move internal result types
vitormattos Sep 4, 2026
4364276
docs(l10n): add context for signature validation
vitormattos Sep 4, 2026
3b25e60
fix(validation): restore signer icon size and remove ghost background
vitormattos Sep 4, 2026
25800a3
fix(validation): use plain glyphs for signer status badges
vitormattos Sep 4, 2026
34051f4
fix(validation): restore required status icons
vitormattos Sep 4, 2026
2192ac7
fix(validation): use list item icon slot as intended
vitormattos Sep 4, 2026
20b5f8c
test(validation): update structured reason expectation
vitormattos Sep 4, 2026
f699d9b
fix(validation): improve signer status icon contrast
vitormattos Sep 4, 2026
b593c3e
fix(validation): clarify certification status icon
vitormattos Sep 4, 2026
0253d6d
test(validation): update status class expectations
vitormattos Sep 4, 2026
1c252e9
fix(validation): ignore expected signature revisions
vitormattos Sep 4, 2026
b6ead51
fix(validation): harden PDF validation presentation
vitormattos Sep 4, 2026
09e6835
refactor(validation): use validator result contract
vitormattos Sep 4, 2026
e6f7eea
fix(validation): map structural validation reasons
vitormattos Sep 5, 2026
3924f0b
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
269ae5b
fix(validation): remove duplicated validation reasons
vitormattos Sep 5, 2026
391880c
refactor(validation): remove unused icon
vitormattos Sep 5, 2026
016f54f
test(validation): cover structural validation reasons
vitormattos Sep 5, 2026
3f09f4a
fix: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
c6e4eb9
test: adapt JSignPdfHandler to jsignpdf-php API
vitormattos Sep 5, 2026
1d4a9ba
style: fix JSignPdfHandler formatting
vitormattos Sep 5, 2026
0bd5fed
fix: preserve signatures without binary payload
vitormattos Sep 5, 2026
1b5d293
test: cover signatures without binary payload
vitormattos Sep 5, 2026
fea229d
fix: keep stable34 JSignPdf handler
vitormattos Sep 6, 2026
b190c4d
test: keep stable34 JSignPdf expectations
vitormattos Sep 6, 2026
12c0b07
fix: use native validation results for certificate chain
vitormattos Sep 6, 2026
5b35a8d
fix: import structured validation reasons
vitormattos Sep 6, 2026
6e48b5d
fix: backport envelope validation note type
vitormattos Sep 6, 2026
3691960
fix: backport CRL validation icon classes
vitormattos Sep 6, 2026
d773f46
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
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