Skip to content

Commit 4393450

Browse files
Merge pull request #4833 from LibreSign/feat/manage-certificate-policy
feat: manage certificate policy
2 parents 552cb85 + 7e39a4e commit 4393450

24 files changed

+2169
-34
lines changed

lib/Controller/AdminController.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
1515
use OCA\Libresign\Helper\ConfigureCheckHelper;
1616
use OCA\Libresign\ResponseDefinitions;
17+
use OCA\Libresign\Service\CertificatePolicyService;
1718
use OCA\Libresign\Service\Install\ConfigureCheckService;
1819
use OCA\Libresign\Service\Install\InstallService;
1920
use OCA\Libresign\Service\SignatureBackgroundService;
@@ -31,6 +32,7 @@
3132
use OCP\IL10N;
3233
use OCP\IRequest;
3334
use OCP\ISession;
35+
use UnexpectedValueException;
3436

3537
/**
3638
* @psalm-import-type LibresignEngineHandler from ResponseDefinitions
@@ -51,6 +53,7 @@ public function __construct(
5153
private IL10N $l10n,
5254
protected ISession $session,
5355
private SignatureBackgroundService $signatureBackgroundService,
56+
private CertificatePolicyService $certificatePolicyService,
5457
) {
5558
parent::__construct(Application::APP_ID, $request);
5659
$this->eventSource = $this->eventSourceFactory->create();
@@ -525,4 +528,101 @@ public function signerName(
525528
);
526529
}
527530
}
531+
532+
/**
533+
* Update certificate policy of this instance
534+
*
535+
* @return DataResponse<Http::STATUS_OK, array{status: 'success', CPS: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{status: 'failure', message: string}, array{}>
536+
*
537+
* 200: OK
538+
* 422: Not found
539+
*/
540+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
541+
public function saveCertificatePolicy(): DataResponse {
542+
$pdf = $this->request->getUploadedFile('pdf');
543+
$phpFileUploadErrors = [
544+
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
545+
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
546+
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
547+
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
548+
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
549+
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
550+
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
551+
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
552+
];
553+
if (empty($pdf)) {
554+
$error = $this->l10n->t('No file uploaded');
555+
} elseif (!empty($pdf) && array_key_exists('error', $pdf) && $pdf['error'] !== UPLOAD_ERR_OK) {
556+
$error = $phpFileUploadErrors[$pdf['error']];
557+
}
558+
if ($error !== null) {
559+
return new DataResponse(
560+
[
561+
'message' => $error,
562+
'status' => 'failure',
563+
],
564+
Http::STATUS_UNPROCESSABLE_ENTITY
565+
);
566+
}
567+
try {
568+
$cps = $this->certificatePolicyService->updateFile($pdf['tmp_name']);
569+
} catch (UnexpectedValueException $e) {
570+
return new DataResponse(
571+
[
572+
'message' => $e->getMessage(),
573+
'status' => 'failure',
574+
],
575+
Http::STATUS_UNPROCESSABLE_ENTITY
576+
);
577+
}
578+
return new DataResponse(
579+
[
580+
'CPS' => $cps,
581+
'status' => 'success',
582+
]
583+
);
584+
}
585+
586+
/**
587+
* Delete certificate policy of this instance
588+
*
589+
* @return DataResponse<Http::STATUS_OK, array{}, array{}>
590+
*
591+
* 200: OK
592+
* 404: Not found
593+
*/
594+
#[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
595+
public function deleteCertificatePolicy(): DataResponse {
596+
$this->certificatePolicyService->deleteFile();
597+
return new DataResponse();
598+
}
599+
600+
/**
601+
* Update OID
602+
*
603+
* @param string $oid OID is a unique numeric identifier for certificate policies in digital certificates.
604+
* @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{status: 'failure', message: string}, array{}>
605+
*
606+
* 200: OK
607+
* 422: Validation error
608+
*/
609+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate-policy/oid', requirements: ['apiVersion' => '(v1)'])]
610+
public function updateOID(string $oid): DataResponse {
611+
try {
612+
$this->certificatePolicyService->updateOid($oid);
613+
return new DataResponse(
614+
[
615+
'status' => 'success',
616+
]
617+
);
618+
} catch (\Exception $e) {
619+
return new DataResponse(
620+
[
621+
'message' => $e->getMessage(),
622+
'status' => 'failure',
623+
],
624+
Http::STATUS_UNPROCESSABLE_ENTITY
625+
);
626+
}
627+
}
528628
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\Controller;
10+
11+
use OCA\Libresign\AppInfo\Application;
12+
use OCA\Libresign\Service\CertificatePolicyService;
13+
use OCP\AppFramework\Controller;
14+
use OCP\AppFramework\Http;
15+
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
16+
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
17+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
18+
use OCP\AppFramework\Http\Attribute\PublicPage;
19+
use OCP\AppFramework\Http\DataResponse;
20+
use OCP\AppFramework\Http\FileDisplayResponse;
21+
use OCP\Files\NotFoundException;
22+
use OCP\IRequest;
23+
24+
class CertificatePolicyController extends Controller {
25+
public function __construct(
26+
IRequest $request,
27+
private CertificatePolicyService $certificatePolicyService,
28+
) {
29+
parent::__construct(Application::APP_ID, $request);
30+
}
31+
32+
/**
33+
* Certificate policy of this instance
34+
*
35+
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Disposition: 'inline; filename="certificate-policy.pdf"', Content-Type: 'application/pdf'}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
36+
*
37+
* 200: OK
38+
* 404: Not found
39+
*/
40+
#[PublicPage]
41+
#[NoCSRFRequired]
42+
#[AnonRateLimit(limit: 10, period: 60)]
43+
#[FrontpageRoute(verb: 'GET', url: '/certificate-policy.pdf')]
44+
public function getCertificatePolicy(): FileDisplayResponse|DataResponse {
45+
try {
46+
$file = $this->certificatePolicyService->getFile();
47+
return new FileDisplayResponse($file, Http::STATUS_OK, [
48+
'Content-Disposition' => 'inline; filename="certificate-policy.pdf"',
49+
'Content-Type' => 'application/pdf',
50+
]);
51+
} catch (NotFoundException $e) {
52+
return new DataResponse([], Http::STATUS_NOT_FOUND);
53+
}
54+
}
55+
}

lib/Handler/CertificateEngine/AEngineHandler.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OCA\Libresign\Exception\InvalidPasswordException;
1414
use OCA\Libresign\Exception\LibresignException;
1515
use OCA\Libresign\Helper\MagicGetterSetterTrait;
16+
use OCA\Libresign\Service\CertificatePolicyService;
1617
use OCP\Files\AppData\IAppDataFactory;
1718
use OCP\Files\IAppData;
1819
use OCP\Files\SimpleFS\ISimpleFolder;
@@ -71,6 +72,7 @@ public function __construct(
7172
protected IAppDataFactory $appDataFactory,
7273
protected IDateTimeFormatter $dateTimeFormatter,
7374
protected ITempManager $tempManager,
75+
protected CertificatePolicyService $certificatePolicyService,
7476
) {
7577
$this->appData = $appDataFactory->get('libresign');
7678
}
@@ -291,6 +293,12 @@ public function setConfigPath(string $configPath): IEngineHandler {
291293
if (!$configPath) {
292294
$this->appConfig->deleteKey(Application::APP_ID, 'config_path');
293295
} else {
296+
if (!is_dir($configPath)) {
297+
mkdir(
298+
directory: $configPath,
299+
recursive: true,
300+
);
301+
}
294302
$this->appConfig->setValueString(Application::APP_ID, 'config_path', $configPath);
295303
}
296304
$this->configPath = $configPath;
@@ -341,6 +349,19 @@ public function configureCheck(): array {
341349
throw new \Exception('Necessary to implement configureCheck method');
342350
}
343351

352+
private function getCertificatePolicy(): array {
353+
$return = ['policySection' => []];
354+
$oid = $this->certificatePolicyService->getOid();
355+
$cps = $this->certificatePolicyService->getCps();
356+
if ($oid && $cps) {
357+
$return['policySection'][] = [
358+
'OID' => $oid,
359+
'CPS' => $cps,
360+
];
361+
}
362+
return $return;
363+
}
364+
344365
public function toArray(): array {
345366
$return = [
346367
'configPath' => $this->getConfigPath(),
@@ -350,6 +371,10 @@ public function toArray(): array {
350371
'names' => [],
351372
],
352373
];
374+
$return = array_merge(
375+
$return,
376+
$this->getCertificatePolicy(),
377+
);
353378
$names = $this->getNames();
354379
foreach ($names as $name => $value) {
355380
$return['rootCert']['names'][] = [

lib/Handler/CertificateEngine/CfsslHandler.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCA\Libresign\Exception\LibresignException;
1717
use OCA\Libresign\Handler\CfsslServerHandler;
1818
use OCA\Libresign\Helper\ConfigureCheckHelper;
19+
use OCA\Libresign\Service\CertificatePolicyService;
1920
use OCA\Libresign\Service\Install\InstallService;
2021
use OCP\Files\AppData\IAppDataFactory;
2122
use OCP\IAppConfig;
@@ -37,7 +38,6 @@ class CfsslHandler extends AEngineHandler implements IEngineHandler {
3738
protected $client;
3839
protected $cfsslUri;
3940
private string $binary = '';
40-
private CfsslServerHandler $cfsslServerHandler;
4141

4242
public function __construct(
4343
protected IConfig $config,
@@ -46,10 +46,11 @@ public function __construct(
4646
protected IAppDataFactory $appDataFactory,
4747
protected IDateTimeFormatter $dateTimeFormatter,
4848
protected ITempManager $tempManager,
49+
protected CfsslServerHandler $cfsslServerHandler,
50+
protected CertificatePolicyService $certificatePolicyService,
4951
) {
50-
parent::__construct($config, $appConfig, $appDataFactory, $dateTimeFormatter, $tempManager);
52+
parent::__construct($config, $appConfig, $appDataFactory, $dateTimeFormatter, $tempManager, $certificatePolicyService);
5153

52-
$this->cfsslServerHandler = new CfsslServerHandler();
5354
$this->cfsslServerHandler->configCallback(fn () => $this->getConfigPath());
5455
}
5556

lib/Handler/CertificateEngine/OpenSslHandler.php

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use OCA\Libresign\Exception\LibresignException;
1212
use OCA\Libresign\Helper\ConfigureCheckHelper;
13+
use OCA\Libresign\Service\CertificatePolicyService;
1314
use OCP\Files\AppData\IAppDataFactory;
1415
use OCP\IAppConfig;
1516
use OCP\IConfig;
@@ -30,8 +31,9 @@ public function __construct(
3031
protected IAppDataFactory $appDataFactory,
3132
protected IDateTimeFormatter $dateTimeFormatter,
3233
protected ITempManager $tempManager,
34+
protected CertificatePolicyService $certificatePolicyService,
3335
) {
34-
parent::__construct($config, $appConfig, $appDataFactory, $dateTimeFormatter, $tempManager);
36+
parent::__construct($config, $appConfig, $appDataFactory, $dateTimeFormatter, $tempManager, $certificatePolicyService);
3537
}
3638

3739
public function generateRootCert(
@@ -44,7 +46,8 @@ public function generateRootCert(
4446
]);
4547

4648
$csr = openssl_csr_new($this->getCsrNames(), $privateKey, ['digest_alg' => 'sha256']);
47-
$x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365 * 5, ['digest_alg' => 'sha256']);
49+
$options = $this->getRootCertOptions();
50+
$x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365 * 5, $options);
4851

4952
openssl_csr_export($csr, $csrout);
5053
openssl_x509_export($x509, $certout);
@@ -57,6 +60,18 @@ public function generateRootCert(
5760
return $pkeyout;
5861
}
5962

63+
private function getRootCertOptions(): array {
64+
$this->generateRootCertConfig();
65+
$configPath = $this->getConfigPath();
66+
67+
$options = [
68+
'digest_alg' => 'sha256',
69+
'config' => $configPath . DIRECTORY_SEPARATOR . 'openssl.cnf',
70+
'x509_extensions' => 'v3_ca',
71+
];
72+
return $options;
73+
}
74+
6075
public function generateCertificate(): string {
6176
$configPath = $this->getConfigPath();
6277
$rootCertificate = file_get_contents($configPath . DIRECTORY_SEPARATOR . 'ca.pem');
@@ -105,10 +120,17 @@ private function getFilenameToLeafCert(): string {
105120
'subjectAltName' => $this->getSubjectAltNames(),
106121
'authorityKeyIdentifier' => 'keyid',
107122
'subjectKeyIdentifier' => 'hash',
108-
// @todo Implement a feature to define this PDF at Administration Settings
109-
// 'certificatePolicies' => '<policyOID> CPS: http://url/with/policy/informations.pdf',
110-
]
123+
],
111124
];
125+
$oid = $this->certificatePolicyService->getOid();
126+
$cps = $this->certificatePolicyService->getCps();
127+
if ($oid && $cps) {
128+
$config['v3_req']['certificatePolicies'] = '@policy_section';
129+
$config['policy_section'] = [
130+
'policyIdentifier' => $oid,
131+
'CPS.1' => $cps,
132+
];
133+
}
112134
if (empty($config['v3_req']['subjectAltName'])) {
113135
unset($config['v3_req']['subjectAltName']);
114136
}
@@ -117,6 +139,34 @@ private function getFilenameToLeafCert(): string {
117139
return $temporaryFile;
118140
}
119141

142+
private function generateRootCertConfig(): void {
143+
// More information about x509v3: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html
144+
$config = [
145+
'v3_ca' => [
146+
'basicConstraints' => 'critical, CA:TRUE',
147+
'keyUsage' => 'critical, digitalSignature, keyCertSign',
148+
'extendedKeyUsage' => 'clientAuth, emailProtection',
149+
'subjectAltName' => $this->getSubjectAltNames(),
150+
'authorityKeyIdentifier' => 'keyid',
151+
'subjectKeyIdentifier' => 'hash',
152+
],
153+
];
154+
$oid = $this->certificatePolicyService->getOid();
155+
$cps = $this->certificatePolicyService->getCps();
156+
if ($oid && $cps) {
157+
$config['v3_ca']['certificatePolicies'] = '@policy_section';
158+
$config['policy_section'] = [
159+
'policyIdentifier' => $oid,
160+
'CPS.1' => $cps,
161+
];
162+
}
163+
if (empty($config['v3_ca']['subjectAltName'])) {
164+
unset($config['v3_ca']['subjectAltName']);
165+
}
166+
$iniContent = $this->arrayToIni($config);
167+
$this->saveFile('openssl.cnf', $iniContent);
168+
}
169+
120170
private function arrayToIni(array $config) {
121171
$fileContent = '';
122172
foreach ($config as $i => $v) {

0 commit comments

Comments
 (0)