Skip to content

Commit bbd8074

Browse files
committed
refactor Let's encrypt issuing and introduce ECC cert handling #5226 #6563
1 parent aae1933 commit bbd8074

File tree

8 files changed

+720
-486
lines changed

8 files changed

+720
-486
lines changed

install/tpl/server.ini.master

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ vhost_proxy_protocol_enabled=n
142142
vhost_proxy_protocol_protocols=ipv4
143143
vhost_proxy_protocol_http_port=880
144144
vhost_proxy_protocol_https_port=8443
145+
le_signature_type=ECDSA
145146
le_auto_cleanup=y
146147
le_auto_cleanup_denylist=[server_name]
147148

interface/lib/classes/validate_domain.inc.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,5 +278,30 @@ function _wildcard_limit() {
278278
return true; // admin may always add wildcard domain
279279
}
280280

281+
/**
282+
* Validates that input is a comma separated list of domain globs.
283+
*/
284+
function domain_glob_list($field_name, $field_value, $validator) {
285+
global $app;
286+
$allowempty = $validator['allowempty'] ?: 'n';
287+
$exceptions = $validator['exceptions'] ?: [];
288+
if (!$field_value) {
289+
if ($allowempty == 'y') {
290+
return '';
291+
}
292+
return $this->get_error($validator['errmsg']);
293+
}
294+
$parts = explode(',', $field_value);
295+
foreach ($parts as $part) {
296+
$part = trim($part);
297+
if (in_array($part, $exceptions, true)) {
298+
continue;
299+
}
300+
if (!preg_match("/^[a-z0-9*._-]+$/i", $part) || !filter_var($part, FILTER_VALIDATE_DOMAIN)) {
301+
return $this->get_error($validator['errmsg']);
302+
}
303+
}
304+
return '';
305+
}
281306

282307
}

interface/web/admin/form/server_config.tform.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,13 +1633,29 @@
16331633
'width' => '40',
16341634
'maxlength' => '255'
16351635
),
1636+
'le_signature_type' => array(
1637+
'datatype' => 'VARCHAR',
1638+
'formtype' => 'SELECT',
1639+
'default' => 'ECDSA',
1640+
'value' => array('RSA' => 'RSA (RSA encryption with SHA-256)', 'ECDSA' => 'ECDSA (Elliptic Curve Digital Signature Algorithm)')
1641+
),
16361642
'le_auto_cleanup' => array(
16371643
'datatype' => 'VARCHAR',
16381644
'formtype' => 'CHECKBOX',
16391645
'default' => 'y',
16401646
'value' => array(0 => 'n', 1 => 'y')
16411647
),
16421648
'le_auto_cleanup_denylist' => array(
1649+
'validators' => array(
1650+
array (
1651+
'type' => 'CUSTOM',
1652+
'class' => 'validate_domain',
1653+
'function' => 'domain_glob_list',
1654+
'allowempty' => 'y',
1655+
'exceptions' => array('[server_name]'),
1656+
'errmsg'=> 'le_auto_cleanup_denylist_error_custom'
1657+
),
1658+
),
16431659
'datatype' => 'VARCHAR',
16441660
'formtype' => 'TEXT',
16451661
'default' => '[server_name]',

interface/web/admin/lib/lang/en_server_config.lng

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
370370
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
371371
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
372372
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
373+
$wb['le_signature_type_txt'] = 'Certificate signature type';
373374
$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
374-
$wb['le_auto_cleanup_denylist_txt'] = 'Comma seperated list of domains that should never be purged.';
375-
$wb['le_auto_cleanup_denylist_note_txt'] = 'Placeholders:';
375+
$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
376+
$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged. <br>E.g. <code>mail.*, externally-managed.example.com</code> <br>Placeholders:';
377+
$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';

interface/web/admin/templates/server_config_web_edit.htm

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,21 @@ <h4 class="panel-title">
248248
<label class="col-sm-3 control-label"><tmpl_var name="skip_le_check_txt"></label>
249249
<div class="col-sm-9"><tmpl_var name="skip_le_check"></div>
250250
</div>
251+
252+
<div class="form-group">
253+
<label class="col-sm-3 control-label">{tmpl_var name='le_signature_type_txt'}</label>
254+
<div class="col-sm-9"><select name="le_signature_type" id="le_signature_type" class="form-control">
255+
{tmpl_var name='le_signature_type'}
256+
</select></div>
257+
</div>
251258
<div class="form-group">
252259
<label class="col-sm-3 control-label"><tmpl_var name="le_auto_cleanup_txt"></label>
253260
<div class="col-sm-9"><tmpl_var name="le_auto_cleanup"></div>
254261
</div>
255262
<div class="form-group">
256263
<label for="le_auto_cleanup_denylist" class="col-sm-3 control-label">{tmpl_var name='le_auto_cleanup_denylist_txt'}</label>
257264
<div class="col-sm-9">
258-
<input type="text" name="CA_path" id="le_auto_cleanup_denylist" value="{tmpl_var name='le_auto_cleanup_denylist'}" class="form-control" />
265+
<input type="text" name="le_auto_cleanup_denylist" id="le_auto_cleanup_denylist" value="{tmpl_var name='le_auto_cleanup_denylist'}" class="form-control" />
259266
<br>{tmpl_var name='le_auto_cleanup_denylist_note_txt'} <a href="javascript:void(0);" class="addPlaceholder">[server_name]</a>
260267
</div>
261268
</div>

server/cli/modules/letsencrypt.inc.php

Lines changed: 177 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,68 +28,211 @@
2828
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929
*/
3030

31-
class letsencrypt_cli extends cli
32-
{
33-
34-
function __construct()
35-
{
36-
$cmd_opt = [];
37-
$cmd_opt['letsencrypt'] = 'showHelp';
38-
$cmd_opt['letsencrypt:list'] = 'list';
31+
class letsencrypt_cli extends cli {
32+
33+
function __construct() {
34+
$cmd_opt = [];
35+
$cmd_opt['letsencrypt'] = 'showHelp';
36+
$cmd_opt['letsencrypt:list'] = 'listCertificates';
37+
$cmd_opt['letsencrypt:info'] = 'outputCertificate';
3938
$cmd_opt['letsencrypt:cleanup-expired'] = 'cleanupExpired';
4039
$this->addCmdOpt($cmd_opt);
4140
}
4241

43-
public function list($arg)
44-
{
42+
public function listCertificates($arg) {
4543
global $app;
4644
$app->uses('letsencrypt');
47-
$certificates = $app->letsencrypt->get_certificate_list();
48-
foreach ($certificates as $certificate) {
49-
print_r($certificate);
45+
$certificates = $this->getCertificates();
46+
if(empty($certificates)) {
47+
return;
48+
}
49+
$table = [['type', 'id', 'valid info', 'serial', 'domains']];
50+
$ansi_reset = "\033[0m";
51+
$bold_red = "\033[1m\033[31m";
52+
$bold_green = "\033[1m\033[32m";
53+
foreach($certificates as $certificate) {
54+
$valid = ($certificate['is_valid'] ? ($bold_green . 'yes' . $ansi_reset) : ($bold_red . 'no ' . $ansi_reset)) . ' ' . $this->getValidInfo($certificate);
55+
$table[] = [
56+
$certificate['signature_type'],
57+
$certificate['id'],
58+
$valid,
59+
$certificate['serial_number'],
60+
$this->getList($certificate['domains']),
61+
];
5062
}
63+
$this->outputTable($table, ['variable_columns' => '1,4', 'expand' => true]);
5164
}
5265

53-
public function cleanupExpired($arg)
54-
{
66+
public function outputCertificate($args) {
5567
global $app;
68+
if(empty($args)) {
69+
70+
}
71+
if(empty($args)) {
72+
$this->swriteln('error: ID of the certificate is missing');
73+
$this->showHelp($args);
74+
exit(1);
75+
}
5676
$app->uses('letsencrypt');
57-
$removals = 0;
58-
$hasErrors = false;
59-
$certificates = $app->letsencrypt->get_certificate_list();
60-
foreach ($certificates as $certificate) {
61-
if (! $certificate['is_valid']) {
62-
$this->swriteln("Removing ".join(', ', $certificate['domains'])." expired certificate...");
63-
if ($app->letsencrypt->remove_certificate($certificate)) {
64-
$removals += 1;
65-
} else {
66-
$this->swriteln("Could not remove ".print_r($certificate, true));
67-
$hasErrors = true;
77+
$certificates = $this->getCertificates();
78+
if(empty($certificates)) {
79+
return;
80+
}
81+
foreach($args as $id) {
82+
$certificate = false;
83+
foreach($certificates as $c) {
84+
if($c['id'] == $id) {
85+
$certificate = $c;
86+
break;
6887
}
6988
}
89+
if($certificate) {
90+
$ansi_reset = "\033[0m";
91+
$bold_red = "\033[1m\033[31m";
92+
$bold_green = "\033[1m\033[32m";
93+
$bold_yellow = "\033[1m\033[33m";
94+
$gray = "\033[38;5;7m";
95+
$valid = ($certificate['is_valid'] ? ($bold_green . 'yes' . $ansi_reset) : ($bold_red . 'no ' . $ansi_reset)) . ' ' . $this->getValidInfo($certificate);
96+
$table = [
97+
['key', 'value'],
98+
['id', $certificate['id']],
99+
['serial', $certificate['serial_number']],
100+
['type', $certificate['signature_type']],
101+
['valid', $valid . "\n" . $gray . 'from ' . $ansi_reset . $certificate['valid_from']->format('Y-m-d H:i:s') . "\n" . $gray . 'to ' . $ansi_reset . $certificate['valid_to']->format('Y-m-d H:i:s')],
102+
['revokation', $certificate['is_revoked'] === null ? ($bold_yellow . 'not checked' . $ansi_reset) : $certificate['is_revoked'] ? ($bold_red . 'REVOKED' . $ansi_reset) : ($bold_green . 'not revoked' . $ansi_reset)],
103+
['domains', $this->getList($certificate['domains'])],
104+
['subject', $this->getAssocArray($certificate['subject'])],
105+
['issuer', $this->getAssocArray($certificate['issuer'])],
106+
['source', $certificate['source']],
107+
['conf', $certificate['conf']],
108+
['files', $this->getAssocArray($certificate['cert_paths'])],
109+
];
110+
$this->outputTable($table, ['min_lengths' => [10], 'variable_columns' => '1', 'expand' => true]);
111+
} else {
112+
$this->swriteln("\n" . 'Certificate not found: ' . $id . "\n");
113+
}
70114
}
71-
if ($removals) {
72-
$this->swriteln("Removed $removals expired certificates");
73-
} else {
74-
$this->swriteln("No certificates were removed");
115+
116+
}
117+
118+
public function cleanupExpired($arg) {
119+
global $app;
120+
$app->uses('letsencrypt');
121+
$removals = 0;
122+
$hasErrors = false;
123+
$certificates = $this->getCertificates();
124+
if(empty($certificates)) {
125+
return;
126+
}
127+
$certificates_to_remove = [];
128+
foreach($certificates as $certificate) {
129+
if(!$certificate['is_valid']) {
130+
$certificates_to_remove[] = $certificate;
131+
}
132+
}
133+
if(empty($certificates_to_remove)) {
134+
$this->swriteln('No expired certificates found');
135+
return;
75136
}
76-
if ($hasErrors) {
137+
$ansi_reset = "\033[0m";
138+
$bold_red = "\033[1m\033[31m";
139+
$table = [['type', 'id', 'valid info', 'serial', 'domains']];
140+
foreach($certificates_to_remove as $certificate) {
141+
$valid = $this->getValidInfo($certificate);
142+
$table[] = [
143+
$certificate['signature_type'],
144+
$certificate['id'],
145+
$valid,
146+
$certificate['serial_number'],
147+
$this->getList($certificate['domains']),
148+
];
149+
}
150+
$this->outputTable($table, ['variable_columns' => '1,4', 'expand' => true]);
151+
$this->swriteln('');
152+
if($this->simple_query($bold_red . 'Do you want to delete the certificates?' . $ansi_reset, ['yes', 'no'], 'no') == 'no') {
153+
$this->swriteln('No certificates were removed');
154+
return;
155+
}
156+
foreach($certificates_to_remove as $certificate) {
157+
$this->swriteln('Removing expired certificate ' . $certificate['id']);
158+
if($app->letsencrypt->remove_certificate($certificate)) {
159+
$removals += 1;
160+
} else {
161+
$this->swriteln("Could not remove " . print_r($certificate, true));
162+
$hasErrors = true;
163+
}
164+
}
165+
$this->swriteln('Removed ' . $removals . ' expired certificates');
166+
if($hasErrors) {
77167
exit(1);
78168
}
79169
}
80170

81-
public function showHelp($arg)
82-
{
171+
public function showHelp($arg) {
83172
global $conf;
84173

85174
$this->swriteln("---------------------------------");
86-
$this->swriteln("- Available commandline option -");
175+
$this->swriteln("- Available commandline options -");
87176
$this->swriteln("---------------------------------");
88177
$this->swriteln("ispc letsencrypt list - lists all known certificates");
178+
$this->swriteln("ispc letsencrypt info <id> - outputs all information of one certificate");
89179
$this->swriteln("ispc letsencrypt cleanup-expired - Cleanup all expired certificates.");
90180
$this->swriteln("---------------------------------");
91181
$this->swriteln();
92182
}
93183

184+
185+
private function getList($array) {
186+
return join("\n", array_map(function($line) {
187+
$ansi_reset = "\033[0m";
188+
$gray = "\033[38;5;7m";
189+
return $gray . '' . $ansi_reset . $line;
190+
}, $array));
191+
}
192+
193+
private function getAssocArray($array) {
194+
return join("\n", array_map(function($key, $value) {
195+
$ansi_reset = "\033[0m";
196+
$gray = "\033[38;5;7m";
197+
return $gray . $key . '=' . $ansi_reset . $value;
198+
}, array_keys($array), array_values($array)));
199+
}
200+
201+
private function getValidInfo($certificate) {
202+
$ansi_reset = "\033[0m";
203+
$bold_red = "\033[1m\033[31m";
204+
$bold_green = "\033[1m\033[32m";
205+
$bold_yellow = "\033[1m\033[33m";
206+
$gray = "\033[38;5;7m";
207+
$now = new DateTime('now');
208+
$diff = $now->diff($certificate['valid_to'])->format('%r%a');
209+
if($diff > 0) {
210+
if($diff <= 7) {
211+
$info = $bold_yellow . $diff . ' day' . ($diff > 1 ? 's' : '') . ' valid' . $ansi_reset;
212+
} else {
213+
$info = $bold_green . $diff . ' days valid' . $ansi_reset;
214+
}
215+
} else {
216+
$diff = abs($diff);
217+
$info = $bold_red . $diff . ' day' . ($diff != 1 ? 's' : '') . ' expired' . $ansi_reset;
218+
}
219+
// $info .= $gray . $certificate['valid_to']->format('Y-m-d H:i:s') . $ansi_reset;
220+
if($certificate['is_revoked'] === null) {
221+
$info .= $gray . ' (no OCSP)' . $ansi_reset;
222+
} elseif($certificate['is_revoked']) {
223+
$info .= $bold_red . ' REVOKED' . $ansi_reset;
224+
}
225+
return $info;
226+
}
227+
228+
private function getCertificates() {
229+
global $app;
230+
$this->swriteln('Getting all certificates…');
231+
$certificates = $app->letsencrypt->get_certificate_list();
232+
if(empty($certificates)) {
233+
$this->swriteln('No certificates found');
234+
}
235+
return $certificates;
236+
}
94237
}
95238

0 commit comments

Comments
 (0)