Skip to content

Commit

Permalink
Merge pull request #4226 from LibreSign/feat/validate-pdf
Browse files Browse the repository at this point in the history
feat: validate pdf
  • Loading branch information
vitormattos authored Jan 2, 2025
2 parents f10e36d + a65d2dd commit 8b05192
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 7 deletions.
1 change: 1 addition & 0 deletions lib/Controller/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ public function signatureGenerate(
$data = [
'user' => [
'host' => $identify,
'uid' => 'account:' . $this->userSession->getUser()->getUID(),
'name' => $this->userSession->getUser()->getDisplayName(),
],
'signPassword' => $signPassword,
Expand Down
1 change: 1 addition & 0 deletions lib/Controller/FileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public function validate(?string $type = null, $identifier = null): DataResponse
->showSigners()
->showSettings()
->showMessages()
->showValidateFile()
->toArray()
);

Expand Down
6 changes: 5 additions & 1 deletion lib/Controller/SignFileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ public function sign(string $method, array $elements = [], string $identifyValue
$this->signFileService
->setLibreSignFile($libreSignFile)
->setSignRequest($signRequest)
->setUserUniqueIdentifier($identifyMethod->getEntity()->getIdentifierValue())
->setUserUniqueIdentifier(
$identifyMethod->getEntity()->getIdentifierKey() .
':' .
$identifyMethod->getEntity()->getIdentifierValue()
)
->setFriendlyName($signRequest->getDisplayName())
->storeUserMetadata([
'user-agent' => $this->request->getHeader('User-Agent'),
Expand Down
11 changes: 11 additions & 0 deletions lib/Handler/CertificateEngine/AEngineHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* @method string getOrganization()
* @method IEngineHandler setOrganizationalUnit(string $organizationalUnit)
* @method string getOrganizationalUnit()
* @method IEngineHandler setUID(string $UID)
* @method string getName()
*/
class AEngineHandler {
Expand All @@ -55,6 +56,7 @@ class AEngineHandler {
protected string $locality = '';
protected string $organization = '';
protected string $organizationalUnit = '';
protected string $UID = '';
protected string $password = '';
protected string $configPath = '';
protected string $engine = '';
Expand Down Expand Up @@ -178,6 +180,8 @@ public function translateToLong($name): string {
return 'Organization';
case 'OU':
return 'OrganizationalUnit';
case 'UID':
return 'UserIdentifier';
}
return '';
}
Expand Down Expand Up @@ -282,12 +286,19 @@ protected function getNames(): array {
'O' => $this->getOrganization(),
'OU' => $this->getOrganizationalUnit(),
];
if ($uid = $this->getUID()) {
$names['UID'] = $uid;
}
$names = array_filter($names, function ($v) {
return !empty($v);
});
return $names;
}

public function getUID(): string {
return str_replace(' ', '+', $this->UID);
}

public function expirity(): int {
$expirity = $this->appConfig->getAppValueInt('expiry_in_days', 365);
if ($expirity < 0) {
Expand Down
8 changes: 8 additions & 0 deletions lib/Handler/CertificateEngine/CfsslHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ public function toArray(): array {
return $return;
}

public function getCommonName(): string {
$uid = $this->getUID();
if (!$uid) {
return $this->commonName;
}
return $uid . ', ' . $this->commonName;
}

private function newCert(): array {
$json = [
'json' => [
Expand Down
2 changes: 2 additions & 0 deletions lib/Handler/CertificateEngine/IEngineHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
* @method string getOrganization()
* @method IEngineHandler setOrganizationalUnit(string $organizationalUnit)
* @method string getOrganizationalUnit()
* @method IEngineHandler setUID(string $UID)
* @method string getUID()
* @method string getName()
*/
interface IEngineHandler {
Expand Down
4 changes: 4 additions & 0 deletions lib/Handler/CertificateEngine/OpenSslHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ private function getCsrNames(): array {
$distinguishedNames['stateOrProvinceName'] = $value;
continue;
}
if ($name === 'UID') {
$distinguishedNames['UID'] = $value;
continue;
}
$longName = $this->translateToLong($name);
$longName = lcfirst($longName) . 'Name';
$distinguishedNames[$longName] = $value;
Expand Down
34 changes: 34 additions & 0 deletions lib/Handler/Pkcs12Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCP\AppFramework\Services\IAppConfig;
use OCP\Files\File;
use OCP\IL10N;
use OCP\ITempManager;
use TypeError;

class Pkcs12Handler extends SignEngineHandler {
Expand All @@ -30,6 +31,7 @@ public function __construct(
private IL10N $l10n,
private JSignPdfHandler $jSignPdfHandler,
private FooterHandler $footerHandler,
private ITempManager $tempManager,
) {
}

Expand Down Expand Up @@ -79,6 +81,37 @@ public function readCertificate(string $uid, string $privateKey): array {
);
}

/**
* @param resource $resource
* @return array
*/
public function validatePdfContent($resource): array {
$content = stream_get_contents($resource);
preg_match_all('/ByteRange\s*\[(\d+) (?<start>\d+) (?<end>\d+) (\d+)?/', $content, $bytes);
if (empty($bytes['start']) || empty($bytes['end'])) {
throw new LibresignException($this->l10n->t('Unsigned file.'));
}

$parsed = [];
for ($i = 0; $i < count($bytes['start']); $i++) {
rewind($resource);
$signature = stream_get_contents(
$resource,
$bytes['end'][$i] - $bytes['start'][$i] - 2,
$bytes['start'][$i] + 1
);
$pfxCertificate = hex2bin($signature);
if (empty($tempFile)) {
$tempFile = $this->tempManager->getTemporaryFile('cert.pfx');
}
file_put_contents($tempFile, $pfxCertificate);
$output = shell_exec("openssl pkcs7 -in {$tempFile} -inform DER -print_certs");
$parsed[] = openssl_x509_parse($output);
}
$this->tempManager->clean();
return $parsed;
}

public function setPfxContent(string $content): void {
$this->pfxContent = $content;
}
Expand Down Expand Up @@ -152,6 +185,7 @@ public function generateCertificate(array $user, string $signPassword, string $f
->setHosts([$user['host']])
->setCommonName($user['name'])
->setFriendlyName($friendlyName)
->setUID($user['uid'])
->setPassword($signPassword)
->generateCertificate();
if (!$content) {
Expand Down
5 changes: 4 additions & 1 deletion lib/Helper/MagicGetterSetterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ public function __call($name, $arguments) {
}
$property = lcfirst($matches['property']);
if (!property_exists($this, $property)) {
throw new \LogicException(sprintf('Cannot set non existing property %s->%s = %s.', \get_class($this), $name, var_export($arguments, true)));
$property = $matches['property'];
if (!property_exists($this, $property)) {
throw new \LogicException(sprintf('Cannot set non existing property %s->%s = %s.', \get_class($this), $name, var_export($arguments, true)));
}
}
if ($matches['type'] === 'get') {
return $this->$property;
Expand Down
1 change: 1 addition & 0 deletions lib/Service/AccountService.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ public function createToSign(string $uuid, string $email, string $password, ?str
$certificate = $this->pkcs12Handler->generateCertificate(
[
'host' => $newUser->getPrimaryEMailAddress(),
'uid' => 'account:' . $newUser->getUID(),
'name' => $newUser->getDisplayName()
],
$signPassword,
Expand Down
67 changes: 67 additions & 0 deletions lib/Service/FileService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
use OCA\Libresign\Db\SignRequest;
use OCA\Libresign\Db\SignRequestMapper;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Handler\Pkcs12Handler;
use OCA\Libresign\Helper\ValidateHelper;
use OCA\Libresign\ResponseDefinitions;
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
use OCP\Accounts\IAccountManager;
use OCP\AppFramework\Services\IAppConfig;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use OCP\IDateTimeFormatter;
Expand All @@ -42,10 +45,12 @@ class FileService {
private bool $showSettings = false;
private bool $showVisibleElements = false;
private bool $showMessages = false;
private bool $validateFile = false;
private ?File $file = null;
private ?SignRequest $signRequest = null;
private ?IUser $me = null;
private ?int $identifyMethodId = null;
private array $certData = [];
private array $signers = [];
private array $settings = [
'canSign' => false,
Expand Down Expand Up @@ -74,6 +79,9 @@ public function __construct(
private IAppConfig $appConfig,
private IURLGenerator $urlGenerator,
protected IMimeTypeDetector $mimeTypeDetector,
protected Pkcs12Handler $pkcs12Handler,
private IUserMountCache $userMountCache,
private IRootFolder $root,
protected LoggerInterface $logger,
protected IL10N $l10n,
) {
Expand Down Expand Up @@ -137,6 +145,11 @@ public function setSignRequest(SignRequest $signRequest): self {
return $this;
}

public function showValidateFile(bool $validateFile = true): self {
$this->validateFile = $validateFile;
return $this;
}

/**
* @return static
*/
Expand All @@ -162,6 +175,7 @@ private function getSigners(): array {
return $this->signers;
}
$signers = $this->signRequestMapper->getByFileId($this->file->getId());
$certData = $this->getCertData();
foreach ($signers as $signer) {
$signatureToShow = [
'signed' => $signer->getSigned() ?
Expand Down Expand Up @@ -191,6 +205,38 @@ private function getSigners(): array {
$data['sign_date'] = (new \DateTime())
->setTimestamp($signer->getSigned())
->format('Y-m-d H:i:s');
$mySignature = array_filter($certData, function ($data) use ($signatureToShow) {
foreach ($signatureToShow['identifyMethods'] as $methods) {
foreach ($methods as $identifyMethod) {
$entity = $identifyMethod->getEntity();
if (array_key_exists('uid', $data['subject'])) {
if ($data['subject']['uid'] === $entity->getIdentifierKey() . ':' . $entity->getIdentifierValue()) {
return true;
}
} else {
preg_match('/(?<key>.*):(?<value>.*), /', $data['subject']['CN'], $matches);
if ($matches) {
if ($matches['key'] === $entity->getIdentifierKey() && $matches['value'] === $entity->getIdentifierValue()) {
return true;
}
}
}
}
}
});
if ($mySignature) {
$mySignature = current($mySignature);
$signatureToShow['subject'] = implode(
', ',
array_map(
fn (string $key, string $value) => "$key: $value",
array_keys($mySignature['subject']),
$mySignature['subject']
)
);
$signatureToShow['valid_from'] = $mySignature['validFrom_time_t'];
$signatureToShow['valid_to'] = $mySignature['validTo_time_t'];
}
}
// @todo refactor this code
if ($this->me || $this->identifyMethodId) {
Expand Down Expand Up @@ -396,6 +442,27 @@ private function getFile(): array {
return $return;
}

private function getCertData(): array {
if (!empty($this->certData) || !$this->validateFile || !$this->file->getSignedNodeId()) {
return $this->certData;
}
$mountsContainingFile = $this->userMountCache->getMountsForFileId($this->file->getSignedNodeId());
foreach ($mountsContainingFile as $fileInfo) {
$this->root->getByIdInPath($this->file->getSignedNodeId(), $fileInfo->getMountPoint());
}
$fileToValidate = $this->root->getById($this->file->getSignedNodeId());
if (!count($fileToValidate)) {
throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404);
}
/** @var \OCP\Files\File */
$file = current($fileToValidate);

$resource = $file->fopen('rb');
$this->certData = $this->pkcs12Handler->validatePdfContent($resource);
fclose($resource);
return $this->certData;
}

/**
* @return string[][]
*
Expand Down
1 change: 1 addition & 0 deletions lib/Service/SignFileService.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ private function getPfxFile(): string {
$certificate = $this->pkcs12Handler->generateCertificate(
[
'host' => $this->userUniqueIdentifier,
'uid' => $this->userUniqueIdentifier,
'name' => $this->friendlyName,
],
$tempPassword,
Expand Down
2 changes: 1 addition & 1 deletion src/views/CreatePassword.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default {
})
.catch(({ response }) => {
this.signMethodsStore.setHasSignatureFile(false)
if (response.data.ocs.data.message) {
if (response.data?.ocs?.data?.message) {
showError(response.data.ocs.data.message)
} else {
showError(t('libresign', 'Error creating new password, please contact the administrator'))
Expand Down
27 changes: 27 additions & 0 deletions src/views/Validation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,33 @@
</ul>
</template>
</NcListItem>
<NcListItem v-if="signer.opened && signer.valid_from"
class="extra"
compact
:name="t('libresign', 'Certificate valid from:')">
<template #name>
<strong>{{ t('libresign', 'Certificate valid from:')}}</strong>
{{ dateFromUnixTimestamp(signer.valid_from) }}
</template>
</NcListItem>
<NcListItem v-if="signer.opened && signer.valid_to"
class="extra"
compact
:name="t('libresign', 'Certificate valid to:')">
<template #name>
<strong>{{ t('libresign', 'Certificate valid to:')}}</strong>
{{ dateFromUnixTimestamp(signer.valid_to) }}
</template>
</NcListItem>
<NcListItem v-if="signer.opened && signer.subject"
class="extra"
compact
:name="t('libresign', 'Subject:')">
<template #name>
<strong>{{ t('libresign', 'Subject:')}}</strong>
{{ signer.subject }}
</template>
</NcListItem>
</span>
</ul>
</div>
Expand Down
2 changes: 2 additions & 0 deletions tests/Api/Controller/SignFileControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ public function testSignUsingFileIdWithEmptyCertificatePassword():void {
$certificate = $pkcs12Handler->generateCertificate(
[
'host' => '[email protected]',
'uid' => 'email:[email protected]',
'name' => 'John Doe',
],
'secretPassword',
Expand Down Expand Up @@ -286,6 +287,7 @@ public function testSignUsingFileIdWithSuccess():void {
$certificate = $pkcs12Handler->generateCertificate(
[
'host' => '[email protected]',
'uid' => 'email:[email protected]',
'name' => 'John Doe',
],
'secretPassword',
Expand Down
Loading

0 comments on commit 8b05192

Please sign in to comment.