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

LibreSign / libresign / 32993735329

26 Aug 2026 05:19PM UTC coverage: 68.022% (+22.7%) from 45.341%
32993735329

push

github

web-flow
Merge pull request #8072 from LibreSign/chore/say-hello-to-nextcloud36

chore: say hello to Nextcloud 36

16292 of 23951 relevant lines covered (68.02%)

11.0 hits per line

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

38.83
/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
        }
89✔
42

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

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

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

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

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

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

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

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

98
        /**
99
         * @return VisibleElementAssoc[]
100
         *
101
         * @psalm-return array<VisibleElementAssoc>
102
         */
103
        public function getVisibleElements(): array {
104
                return $this->visibleElements;
15✔
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;
35✔
115
        }
116

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

123
        public function setLeafExpiryOverrideInDays(?int $days): self {
124
                $this->getCertificateEngine()->setLeafExpiryOverrideInDays($days);
×
125
                return $this;
×
126
        }
127

128
        /**
129
         * Generate certificate
130
         *
131
         * @param array $user Example: ['host' => '', 'name' => '']
132
         * @param string $signPassword Password of signature
133
         * @param string $friendlyName Friendly name
134
         */
135
        public function generateCertificate(array $user, string $signPassword, string $friendlyName): string {
136
                try {
137
                        $content = $this->getCertificateEngine()
×
138
                                ->setHosts([$user['host']])
×
139
                                ->setCommonName($user['name'])
×
140
                                ->setFriendlyName($friendlyName)
×
141
                                ->setUID($user['uid'])
×
142
                                ->setPassword($signPassword)
×
143
                                ->generateCertificate();
×
144
                } catch (LibresignException $e) {
×
145
                        throw $e;
×
146
                } catch (EmptyCertificateException) {
×
147
                        // TRANSLATORS Error while LibreSign creates a user's signing certificate (PFX): the instance root CA material is missing or empty.
148
                        throw new LibresignException($this->l10n->t('Empty root certificate data'));
×
149
                } catch (InvalidArgumentException) {
×
150
                        // TRANSLATORS Error while LibreSign creates a user's signing certificate: the identity fields or password payload sent to the certificate engine are invalid.
151
                        throw new LibresignException($this->l10n->t('Invalid data to generate certificate'));
×
152
                } catch (\Throwable) {
×
153
                        // TRANSLATORS Error while LibreSign creates a user's signing certificate: the certificate engine failed unexpectedly during generation.
154
                        throw new LibresignException($this->l10n->t('Failure on generate certificate'));
×
155
                }
156
                if (!$content) {
×
157
                        // TRANSLATORS Error while LibreSign creates a user's signing certificate: generation finished without returning a PFX/certificate payload.
158
                        throw new LibresignException($this->l10n->t('Failure to generate certificate'));
×
159
                }
160
                $this->setCertificate($content);
×
161
                return $content;
×
162
        }
163

164
        public function savePfx(string $uid, string $content): string {
165
                $this->folderService->setUserId($uid);
4✔
166
                $folder = $this->folderService->getFolder();
4✔
167

168
                try {
169
                        $folder->newFile($this->pfxFilename, $content);
4✔
170
                } catch (NotPermittedException) {
1✔
171
                        // TRANSLATORS Permission error when LibreSign tries to store the user's signing certificate (PFX) in their Nextcloud home folder.
172
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
1✔
173
                }
174

175
                return $content;
3✔
176
        }
177

178
        public function deletePfx(string $uid): void {
179
                $this->folderService->setUserId($uid);
×
180
                $folder = $this->folderService->getFolder();
×
181
                try {
182
                        $file = $folder->get($this->pfxFilename);
×
183
                        $file->delete();
×
184
                } catch (NotPermittedException) {
×
185
                        // TRANSLATORS Permission error when LibreSign tries to delete the user's signing certificate (PFX) from their Nextcloud home folder.
186
                        throw new LibresignException($this->l10n->t('You do not have permission for this action.'));
×
187
                } catch (NotFoundException|InvalidPathException) {
×
188
                }
189
        }
190

191
        /**
192
         * Get content of pfx file
193
         */
194
        public function getPfxOfCurrentSigner(?string $uid = null): string {
195
                if (!empty($this->certificate) || empty($uid)) {
32✔
196
                        return $this->certificate;
23✔
197
                }
198
                $this->folderService->setUserId($uid);
10✔
199
                $folder = $this->folderService->getFolder();
10✔
200
                try {
201
                        /** @var \OCP\Files\File */
202
                        $node = $folder->get($this->pfxFilename);
10✔
203
                        $this->certificate = $node->getContent();
3✔
204
                } catch (GenericFileException|NotFoundException) {
9✔
205
                        // TRANSLATORS Error shown to the signer when LibreSign has no stored signing certificate/password yet; they must create a signing password before signing.
206
                        throw new LibresignException($this->l10n->t('Password to sign not defined. Create a password to sign.'), 400);
7✔
207
                } catch (\Throwable) {
2✔
208
                }
209
                if (empty($this->certificate)) {
3✔
210
                        // TRANSLATORS Error shown to the signer when LibreSign has no stored signing certificate/password yet; they must create a signing password before signing.
211
                        throw new LibresignException($this->l10n->t('Password to sign not defined. Create a password to sign.'), 400);
2✔
212
                }
213
                if ($this->getPassword()) {
1✔
214
                        try {
215
                                $this->getCertificateEngine()->readCertificate($this->certificate, $this->getPassword());
×
216
                        } catch (InvalidPasswordException) {
×
217
                                // TRANSLATORS Error shown to the signer when the password entered to unlock their LibreSign signing certificate (PFX) is wrong.
218
                                throw new LibresignException($this->l10n->t('Invalid password'));
×
219
                        }
220
                }
221
                return $this->certificate;
1✔
222
        }
223

224
        public function updatePassword(string $uid, string $currentPrivateKey, string $newPrivateKey): string {
225
                $pfx = $this->getPfxOfCurrentSigner($uid);
×
226
                $content = $this->getCertificateEngine()->updatePassword(
×
227
                        $pfx,
×
228
                        $currentPrivateKey,
×
229
                        $newPrivateKey
×
230
                );
×
231
                return $this->savePfx($uid, $content);
×
232
        }
233

234
        #[\Override]
235
        public function getLastSignedDate(): \DateTime {
236
                $stream = $this->getFileStream();
1✔
237

238
                $chain = $this->getCertificateChain($stream);
×
239
                if (empty($chain)) {
×
240
                        throw new \UnexpectedValueException('Certificate chain is empty.');
×
241
                }
242

243
                $last = $chain[array_key_last($chain)];
×
244
                if (!is_array($last) || !isset($last['signingTime']) || !$last['signingTime'] instanceof \DateTime) {
×
245
                        $this->logger->error('Invalid signingTime in certificate chain.', ['chain' => $chain]);
×
246
                        throw new \UnexpectedValueException('Invalid signingTime in certificate chain.');
×
247
                }
248

249
                // Prevent accepting certificates with future signing dates (possible clock issues)
250
                $dateTime = new \DateTime('now', new \DateTimeZone('UTC'));
×
251
                if ($last['signingTime'] > $dateTime) {
×
252
                        $this->logger->error('We found Marty McFly', [
×
253
                                'last_signature' => json_encode($last['signingTime']),
×
254
                                'current_date_time' => json_encode($dateTime),
×
255
                        ]);
×
256
                        throw new \UnexpectedValueException('Invalid signingTime in certificate chain. We found Marty McFly');
×
257
                }
258

259
                return $last['signingTime'];
×
260
        }
261

262
        /**
263
         * @return resource
264
         */
265
        protected function getFileStream() {
266
                $signedFile = $this->getInputFile();
1✔
267
                $stream = $signedFile->fopen('rb');
×
268
                if ($stream === false) {
×
269
                        throw new \RuntimeException('Unable to open the signed file for reading.');
×
270
                }
271
                return $stream;
×
272
        }
273

274
        protected function getCertificateEngineFactory(): CertificateEngineFactory {
275
                return \OCP\Server::get(CertificateEngineFactory::class);
1✔
276
        }
277

278
        protected function getCertificateEngine(): IEngineHandler {
279
                return $this->getCertificateEngineFactory()->getEngine();
21✔
280
        }
281

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

© 2026 Coveralls, Inc