• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 19860386532

02 Dec 2025 01:32PM UTC coverage: 40.694%. First build
19860386532

Pull #5913

github

web-flow
Merge b864ef164 into e5b626f32
Pull Request #5913: feat: improve certificate error handling

0 of 4 new or added lines in 2 files covered. (0.0%)

4963 of 12196 relevant lines covered (40.69%)

3.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.56
/lib/Handler/SignEngine/SignEngineHandler.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Handler\SignEngine;
10

11
use InvalidArgumentException;
12
use OCA\Libresign\DataObjects\VisibleElementAssoc;
13
use OCA\Libresign\Exception\EmptyCertificateException;
14
use OCA\Libresign\Exception\InvalidPasswordException;
15
use OCA\Libresign\Exception\LibresignException;
16
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
17
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
18
use OCA\Libresign\Service\FolderService;
19
use OCP\Files\File;
20
use OCP\Files\GenericFileException;
21
use OCP\Files\InvalidPathException;
22
use OCP\Files\NotFoundException;
23
use OCP\Files\NotPermittedException;
24
use OCP\IL10N;
25
use Psr\Log\LoggerInterface;
26

27
abstract class SignEngineHandler implements ISignEngineHandler {
28
        private File $inputFile;
29
        protected string $certificate = '';
30
        private string $pfxFilename = 'signature.pfx';
31
        private string $password = '';
32
        /** @var VisibleElementAssoc[] */
33
        private array $visibleElements = [];
34
        private array $signatureParams = [];
35

36
        public function __construct(
37
                private IL10N $l10n,
38
                private readonly FolderService $folderService,
39
                private LoggerInterface $logger,
40
        ) {
41
        }
53✔
42

43
        /**
44
         * @return static
45
         */
46
        #[\Override]
47
        public function setInputFile(File $inputFile): self {
48
                $this->inputFile = $inputFile;
12✔
49
                return $this;
12✔
50
        }
51

52
        #[\Override]
53
        public function getInputFile(): File {
54
                return $this->inputFile;
2✔
55
        }
56

57
        #[\Override]
58
        public function setCertificate(string $certificate): self {
59
                $this->certificate = $certificate;
16✔
60
                return $this;
16✔
61
        }
62

63
        #[\Override]
64
        public function getCertificate(): string {
65
                return $this->certificate;
13✔
66
        }
67

68
        #[\Override]
69
        public function readCertificate(): array {
70
                return $this->getCertificateEngine()
1✔
71
                        ->readCertificate(
1✔
72
                                $this->getCertificate(),
1✔
73
                                $this->getPassword()
1✔
74
                        );
1✔
75
        }
76

77
        #[\Override]
78
        public function setPassword(string $password): self {
79
                $this->password = $password;
17✔
80
                return $this;
17✔
81
        }
82

83
        #[\Override]
84
        public function getPassword(): string {
85
                return $this->password;
3✔
86
        }
87

88
        /**
89
         * @param VisibleElementAssoc[] $visibleElements
90
         *
91
         * @return static
92
         */
93
        public function setVisibleElements(array $visibleElements): self {
94
                $this->visibleElements = $visibleElements;
2✔
95
                return $this;
2✔
96
        }
97

98
        /**
99
         * @return VisibleElementAssoc[]
100
         *
101
         * @psalm-return array<VisibleElementAssoc>
102
         */
103
        public function getVisibleElements(): array {
104
                return $this->visibleElements;
×
105
        }
106

107
        #[\Override]
108
        public function getSignedContent(): string {
109
                return $this->sign()->getContent();
×
110
        }
111

112
        #[\Override]
113
        public function getSignatureParams(): array {
114
                return $this->signatureParams;
5✔
115
        }
116

117
        #[\Override]
118
        public function setSignatureParams(array $params): self {
119
                $this->signatureParams = $params;
2✔
120
                return $this;
2✔
121
        }
122

123
        /**
124
         * Generate certificate
125
         *
126
         * @param array $user Example: ['host' => '', 'name' => '']
127
         * @param string $signPassword Password of signature
128
         * @param string $friendlyName Friendly name
129
         */
130
        public function generateCertificate(array $user, string $signPassword, string $friendlyName): string {
131
                try {
132
                        $content = $this->getCertificateEngine()
×
133
                                ->setHosts([$user['host']])
×
134
                                ->setCommonName($user['name'])
×
135
                                ->setFriendlyName($friendlyName)
×
136
                                ->setUID($user['uid'])
×
137
                                ->setPassword($signPassword)
×
138
                                ->generateCertificate();
×
NEW
139
                } catch (LibresignException $e) {
×
NEW
140
                        throw $e;
×
141
                } catch (EmptyCertificateException) {
×
142
                        throw new LibresignException($this->l10n->t('Empty root certificate data'));
×
143
                } catch (InvalidArgumentException) {
×
144
                        throw new LibresignException($this->l10n->t('Invalid data to generate certificate'));
×
145
                } catch (\Throwable) {
×
146
                        throw new LibresignException($this->l10n->t('Failure on generate certificate'));
×
147
                }
148
                if (!$content) {
×
149
                        throw new LibresignException($this->l10n->t('Failure to generate certificate'));
×
150
                }
151
                $this->setCertificate($content);
×
152
                return $content;
×
153
        }
154

155
        public function savePfx(string $uid, string $content): string {
156
                $this->folderService->setUserId($uid);
3✔
157
                $folder = $this->folderService->getFolder();
3✔
158

159
                try {
160
                        $folder->newFile($this->pfxFilename, $content);
3✔
161
                } catch (NotPermittedException) {
1✔
162
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
163
                }
164

165
                return $content;
2✔
166
        }
167

168
        public function deletePfx(string $uid): void {
169
                $this->folderService->setUserId($uid);
×
170
                $folder = $this->folderService->getFolder();
×
171
                try {
172
                        $file = $folder->get($this->pfxFilename);
×
173
                        $file->delete();
×
174
                } catch (NotPermittedException) {
×
175
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
×
176
                } catch (NotFoundException|InvalidPathException) {
×
177
                }
178
        }
179

180
        /**
181
         * Get content of pfx file
182
         */
183
        public function getPfxOfCurrentSigner(?string $uid = null): string {
184
                if (!empty($this->certificate) || empty($uid)) {
20✔
185
                        return $this->certificate;
11✔
186
                }
187
                $this->folderService->setUserId($uid);
9✔
188
                $folder = $this->folderService->getFolder();
9✔
189
                try {
190
                        /** @var \OCP\Files\File */
191
                        $node = $folder->get($this->pfxFilename);
9✔
192
                        $this->certificate = $node->getContent();
3✔
193
                } catch (GenericFileException|NotFoundException) {
8✔
194
                        throw new LibresignException($this->l10n->t('Password to sign not defined. Create a password to sign.'), 400);
6✔
195
                } catch (\Throwable) {
2✔
196
                }
197
                if (empty($this->certificate)) {
3✔
198
                        throw new LibresignException($this->l10n->t('Password to sign not defined. Create a password to sign.'), 400);
2✔
199
                }
200
                if ($this->getPassword()) {
1✔
201
                        try {
202
                                $this->getCertificateEngine()->readCertificate($this->certificate, $this->getPassword());
×
203
                        } catch (InvalidPasswordException) {
×
204
                                throw new LibresignException($this->l10n->t('Invalid password'));
×
205
                        }
206
                }
207
                return $this->certificate;
1✔
208
        }
209

210
        public function updatePassword(string $uid, string $currentPrivateKey, string $newPrivateKey): string {
211
                $pfx = $this->getPfxOfCurrentSigner($uid);
×
212
                $content = $this->getCertificateEngine()->updatePassword(
×
213
                        $pfx,
×
214
                        $currentPrivateKey,
×
215
                        $newPrivateKey
×
216
                );
×
217
                return $this->savePfx($uid, $content);
×
218
        }
219

220
        #[\Override]
221
        public function getLastSignedDate(): \DateTime {
222
                $stream = $this->getFileStream();
1✔
223

224
                $chain = $this->getCertificateChain($stream);
×
225
                if (empty($chain)) {
×
226
                        throw new \UnexpectedValueException('Certificate chain is empty.');
×
227
                }
228

229
                $last = $chain[array_key_last($chain)];
×
230
                if (!is_array($last) || !isset($last['signingTime']) || !$last['signingTime'] instanceof \DateTime) {
×
231
                        $this->logger->error('Invalid signingTime in certificate chain.', ['chain' => $chain]);
×
232
                        throw new \UnexpectedValueException('Invalid signingTime in certificate chain.');
×
233
                }
234

235
                // Prevent accepting certificates with future signing dates (possible clock issues)
236
                $dateTime = new \DateTime('now', new \DateTimeZone('UTC'));
×
237
                if ($last['signingTime'] > $dateTime) {
×
238
                        $this->logger->error('We found Marty McFly', [
×
239
                                'last_signature' => json_encode($last['signingTime']),
×
240
                                'current_date_time' => json_encode($dateTime),
×
241
                        ]);
×
242
                        throw new \UnexpectedValueException('Invalid signingTime in certificate chain. We found Marty McFly');
×
243
                }
244

245
                return $last['signingTime'];
×
246
        }
247

248
        /**
249
         * @return resource
250
         */
251
        protected function getFileStream() {
252
                $signedFile = $this->getInputFile();
1✔
253
                $stream = $signedFile->fopen('rb');
×
254
                if ($stream === false) {
×
255
                        throw new \RuntimeException('Unable to open the signed file for reading.');
×
256
                }
257
                return $stream;
×
258
        }
259

260
        protected function getCertificateEngine(): IEngineHandler {
261
                return \OCP\Server::get(CertificateEngineFactory::class)
5✔
262
                        ->getEngine();
5✔
263
        }
264

265
        protected function beforeSign(): void {
266
                $this->getCertificateEngine()->validateRootCertificate();
2✔
267
        }
268
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc