Skip to content

Commit 2a5fdc9

Browse files
committed
LE: automatically add certificates of other services (ISPConfig interface, Postfix, Pure-FTPd) to the deny list #5226 #6563
1 parent 5a4ec5c commit 2a5fdc9

File tree

2 files changed

+65
-14
lines changed

2 files changed

+65
-14
lines changed

server/cli/modules/letsencrypt.inc.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ public function listCertificates($arg) {
6565

6666
public function outputCertificate($args) {
6767
global $app;
68-
if(empty($args)) {
6968

70-
}
7169
if(empty($args)) {
7270
$this->swriteln('error: ID of the certificate is missing');
7371
$this->showHelp($args);
@@ -93,6 +91,7 @@ public function outputCertificate($args) {
9391
$bold_yellow = "\033[1m\033[33m";
9492
$gray = "\033[38;5;7m";
9593
$valid = ($certificate['is_valid'] ? ($bold_green . 'yes' . $ansi_reset) : ($bold_red . 'no ' . $ansi_reset)) . ' ' . $this->getValidInfo($certificate);
94+
$on_deny_list = $app->letsencrypt->check_deny_list($certificate);
9695
$table = [
9796
['key', 'value'],
9897
['id', $certificate['id']],
@@ -110,6 +109,7 @@ public function outputCertificate($args) {
110109
['source', $certificate['source']],
111110
['conf', $certificate['conf']],
112111
['files', $this->getAssocArray($certificate['cert_paths'])],
112+
['deny_list', empty($on_deny_list) ? ($bold_green . 'no' . $ansi_reset) : $bold_red . 'yes' . $ansi_reset . "\n" . $this->getList($on_deny_list)],
113113
];
114114
$this->outputTable($table, ['min_lengths' => [10], 'variable_columns' => '1', 'expand' => true]);
115115
} else {

server/lib/classes/letsencrypt.inc.php

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -666,50 +666,76 @@ public function get_certificate_list() {
666666
return $certificates;
667667
}
668668

669-
private $_deny_list = null;
669+
/** @var array|null */
670+
private $_deny_list_domains = null;
671+
/** @var array|null */
672+
private $_deny_list_serials = null;
670673

671674
private function get_deny_list() {
672675
global $app, $conf;
673676

674-
if(is_null($this->_deny_list)) {
677+
if(is_null($this->_deny_list_domains)) {
675678
$server_db_record = $app->db->queryOneRecord("SELECT * FROM server WHERE server_id = ?", $conf['server_id']);
676679
$app->uses('getconf');
677680
$web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
678681

679-
$this->_deny_list = empty($web_config['le_auto_cleanup_denylist']) ? [] : array_filter(array_map(function($pattern) use ($server_db_record) {
682+
$this->_deny_list_domains = empty($web_config['le_auto_cleanup_denylist']) ? [] : array_filter(array_map(function($pattern) use ($server_db_record) {
680683
$pattern = trim($pattern);
681684
if($server_db_record && $pattern == '[server_name]') {
682685
return $server_db_record['server_name'];
683686
}
684687

685688
return $pattern;
686689
}, explode(',', $web_config['le_auto_cleanup_denylist'])));
690+
691+
$this->_deny_list_domains = array_values(array_unique($this->_deny_list_domains));
692+
693+
// search certificates the installer creates and automatically add their serial numbers to deny list
694+
$this->_deny_list_serials = [];
695+
foreach([
696+
'/usr/local/ispconfig/interface/ssl/ispserver.crt',
697+
'/etc/postfix/smtpd.cert',
698+
'/etc/ssl/private/pure-ftpd.pem'
699+
] as $possible_cert_file) {
700+
$cert = $this->extract_first_certificate($possible_cert_file);
701+
if($cert) {
702+
$info = $this->extract_x509($cert);
703+
if($info) {
704+
$app->log('add serial number ' . $info['serial_number'] . ' from ' . $possible_cert_file . ' to deny list', LOGLEVEL_DEBUG);
705+
$this->_deny_list_serials[] = $info['serial_number'];
706+
}
707+
}
708+
}
709+
$this->_deny_list_serials = array_values(array_unique($this->_deny_list_serials));
687710
}
688-
return $this->_deny_list;
711+
return [$this->_deny_list_domains, $this->_deny_list_serials];
689712
}
690713

691714
/**
692715
* Checks if $certificate is on the deny list or has a wildcard domain.
693-
* Returns an array of the deny list patterns that matched the certificate.
716+
* Returns an array of the deny list patterns and serials numbers that matched the certificate.
694717
* An empty array means that the $certificate is not on the deny list.
695718
*
696719
* @param array $certificate
697720
* @return array
698721
*/
699722
public function check_deny_list($certificate) {
700-
$deny_list = $this->get_deny_list();
723+
list($deny_list_domains, $deny_list_serials) = $this->get_deny_list();
701724
$on_deny_list = [];
702725
foreach($certificate['domains'] as $cert_domain) {
703726
if(substr($cert_domain, 0, 2) == '*.') {
704727
// wildcard domains are always on the deny list
705728
$on_deny_list[] = $cert_domain;
706729
} else {
707-
$on_deny_list = array_merge($on_deny_list, array_filter($deny_list, function($deny_pattern) use ($cert_domain) {
730+
$on_deny_list = array_merge($on_deny_list, array_filter($deny_list_domains, function($deny_pattern) use ($cert_domain) {
708731
return mb_strtolower($deny_pattern) == mb_strtolower($cert_domain) || fnmatch($deny_pattern, $cert_domain, FNM_CASEFOLD);
709732
}));
710733
}
711734
}
712-
return array_values(array_unique($on_deny_list));
735+
if(in_array($certificate['serial_number'], $deny_list_serials, true)) {
736+
$on_deny_list[] = $certificate['serial_number'];
737+
}
738+
return $on_deny_list;
713739
}
714740

715741
/**
@@ -728,6 +754,11 @@ public function remove_certificate($certificate, $revoke_before_delete = null, $
728754
$revoke_before_delete = !empty($web_config['le_revoke_before_delete']) && $web_config['le_revoke_before_delete'] == 'y';
729755
}
730756

757+
if($certificate['is_revoked'] && $revoke_before_delete) {
758+
$revoke_before_delete = false;
759+
$app->log('remove_certificate: skip revokation of ' . $certificate['id'] . ' because it already is revoked', LOGLEVEL_DEBUG);
760+
}
761+
731762
if($check_deny_list) {
732763
$on_deny_list = $this->check_deny_list($certificate);
733764
if(!empty($on_deny_list)) {
@@ -805,15 +836,20 @@ public function remove_certificate($certificate, $revoke_before_delete = null, $
805836
return true;
806837
}
807838

808-
public function extract_x509($cert_file, $chain_file = null) {
839+
public function extract_x509($cert_file_or_contents, $chain_file = null) {
809840
global $app;
810841
if(!function_exists('openssl_x509_parse')) {
811842
$app->log('extract_x509: openssl extension missing', LOGLEVEL_ERROR);
812843
return false;
813844
}
814-
$info = openssl_x509_parse(file_get_contents($cert_file), true);
845+
$cert_file = false;
846+
if(strpos($cert_file_or_contents, '-----BEGIN CERTIFICATE-----') === false) {
847+
$cert_file = $cert_file_or_contents;
848+
$cert_file_or_contents = file_get_contents($cert_file_or_contents);
849+
}
850+
$info = openssl_x509_parse($cert_file_or_contents, true);
815851
if(!$info) {
816-
$app->log('extract_x509: ' . $cert_file . ' could not be parsed', LOGLEVEL_ERROR);
852+
$app->log('extract_x509: ' . ($cert_file ?: 'inline certificate') . ' could not be parsed', LOGLEVEL_ERROR);
817853
return false;
818854
}
819855
if(empty($info['subject']['CN']) || !$this->is_domain_name_or_wildcard($info['subject']['CN'])) {
@@ -848,7 +884,7 @@ public function extract_x509($cert_file, $chain_file = null) {
848884
$is_valid = $valid_from <= $now && $now <= $valid_to;
849885
$is_revoked = null;
850886
// only do online revokation check when cert is valid and we got the required chain
851-
if($is_valid && $this->is_readable_link_or_file($chain_file)) {
887+
if($is_valid && $cert_file && $this->is_readable_link_or_file($chain_file)) {
852888
$ocsp_uri = $app->system->exec_safe('openssl x509 -noout -ocsp_uri -in ? 2>&1', $cert_file);
853889
$ocsp_host = parse_url($ocsp_uri ?: '', PHP_URL_HOST);
854890
if($ocsp_uri && $ocsp_host) {
@@ -881,6 +917,21 @@ public function extract_x509($cert_file, $chain_file = null) {
881917
];
882918
}
883919

920+
private function extract_first_certificate($file) {
921+
if(!$this->is_readable_link_or_file($file)) {
922+
return false;
923+
}
924+
$contents = file_get_contents($file);
925+
if(!$contents) {
926+
return false;
927+
}
928+
$matches = [];
929+
if(!preg_match('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/ms', $contents, $matches)) {
930+
return false;
931+
}
932+
return $matches[0];
933+
}
934+
884935
private function is_domain_name_or_wildcard($input) {
885936
$input = filter_var($input, FILTER_VALIDATE_DOMAIN);
886937
if(!$input) {

0 commit comments

Comments
 (0)