Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
CfsslServerHandler
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 7
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configCallback
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 createConfigServer
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 putCsrServer
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 saveNewConfig
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
 saveConfig
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 updateExpirity
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Handler;
10
11use Closure;
12use OCA\Libresign\Exception\LibresignException;
13use OCA\Libresign\Service\CertificatePolicyService;
14
15class CfsslServerHandler {
16    private string $csrServerFile = '';
17    private string $configServerFile = '';
18    private string $configServerFileHash = '';
19    private string $crlUrl = '';
20    private Closure $getCurrentConfigPath;
21
22    public function __construct(
23        private CertificatePolicyService $certificatePolicyService,
24    ) {
25    }
26
27    /**
28     * Create a callback to get config path not at the constructor because
29     * getting at constructor, every time that the class is instantiated, will
30     * try to create the config path if not exists.
31     */
32    public function configCallback(Closure $callback): void {
33        $this->getCurrentConfigPath = $callback;
34        $this->getCurrentConfigPath = function () use ($callback): void {
35            if ($this->csrServerFile) {
36                return;
37            }
38            $configPath = $callback();
39            $this->csrServerFile = $configPath . DIRECTORY_SEPARATOR . 'csr_server.json';
40            $this->configServerFile = $configPath . DIRECTORY_SEPARATOR . 'config_server.json';
41            $this->configServerFileHash = $configPath . DIRECTORY_SEPARATOR . 'hashes.sha256';
42        };
43    }
44
45    public function createConfigServer(
46        string $commonName,
47        array $names,
48        int $expirityInDays,
49        string $crlUrl = '',
50    ): void {
51        $this->crlUrl = $crlUrl;
52        $this->putCsrServer(
53            $commonName,
54            $names,
55            $expirityInDays,
56            $crlUrl,
57        );
58        $this->saveNewConfig($expirityInDays);
59    }
60
61    private function putCsrServer(
62        string $commonName,
63        array $names,
64        int $expirityInDays,
65        string $crlUrl,
66    ): void {
67        $content = [
68            'CN' => $commonName,
69            'key' => [
70                'algo' => 'rsa',
71                'size' => 2048,
72            ],
73            'ca' => [
74                'expiry' => ($expirityInDays * 24) . 'h',
75                /**
76                 * Look the RFC about pathlen constraint
77                 *
78                 * @link https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9
79                 */
80                'pathlen' => 1,
81            ],
82            'names' => [],
83        ];
84        if (!empty($crlUrl)) {
85            $content['crl_url'] = $crlUrl;
86        }
87        foreach ($names as $id => $name) {
88            $content['names'][0][$id] = is_array($name['value']) ? implode(', ', $name['value']) : $name['value'];
89        }
90        ($this->getCurrentConfigPath)();
91        $response = file_put_contents($this->csrServerFile, json_encode($content));
92        if ($response === false) {
93            throw new LibresignException(
94                "Error while writing CSR server file.\n"
95                . 'Remove the CFSSL API URI and Config path to use the default values.',
96                500
97            );
98        }
99    }
100    private function saveNewConfig(int $expirity): void {
101        $config = [
102            'signing' => [
103                'profiles' => [
104                    'client' => [
105                        'expiry' => ($expirity * 24) . 'h',
106                        'usages' => [
107                            'signing',
108                            'digital signature',
109                            'content commitment',
110                            'key encipherment',
111                            'client auth',
112                            'email protection'
113                        ],
114                        'ca_constraint' => [
115                            'is_ca' => false
116                        ],
117                    ],
118                ],
119            ],
120        ];
121
122        if (!empty($this->crlUrl)) {
123            $config['signing']['profiles']['client']['crl_url'] = $this->crlUrl;
124        }
125
126        $oid = $this->certificatePolicyService->getOid();
127        $cps = $this->certificatePolicyService->getCps();
128        if ($oid && $cps) {
129            $config['signing']['profiles']['client']['policies'][] = [
130                'id' => $oid,
131                'qualifiers' => [
132                    [
133                        'type' => 'id-qt-cps',
134                        'value' => $cps,
135                    ],
136                ],
137            ];
138        }
139        $this->saveConfig($config);
140    }
141
142    private function saveConfig(array $config): void {
143        $jsonConfig = json_encode($config);
144        ($this->getCurrentConfigPath)();
145        $response = file_put_contents($this->configServerFile, $jsonConfig);
146        if ($response === false) {
147            throw new LibresignException('Error while writing config server file!', 500);
148        }
149        $hash = hash('sha256', $jsonConfig) . ' config_server.json';
150        file_put_contents($this->configServerFileHash, $hash);
151    }
152
153    public function updateExpirity(int $expirity): void {
154        ($this->getCurrentConfigPath)();
155        if (file_exists($this->configServerFileHash)) {
156            $hashes = file_get_contents($this->configServerFileHash);
157            preg_match('/(?<hash>\w*) +config_server.json/', $hashes, $matches);
158            $savedHash = $matches['hash'];
159        } else {
160            $savedHash = '';
161        }
162        $jsonConfig = file_get_contents($this->configServerFile);
163        $currentHash = hash('sha256', $jsonConfig);
164        if ($currentHash === $savedHash) {
165            return;
166        }
167        $config = json_decode($jsonConfig, true);
168        $config['signing']['profiles']['CA']['expiry'] = ($expirity * 24) . 'h';
169        $config['signing']['profiles']['client']['expiry'] = ($expirity * 24) . 'h';
170        $this->saveConfig($config);
171    }
172}