diff --git a/actions/admin/settings/131.ssl.php b/actions/admin/settings/131.ssl.php index 3300f567..5c37869c 100644 --- a/actions/admin/settings/131.ssl.php +++ b/actions/admin/settings/131.ssl.php @@ -185,12 +185,18 @@ return array( 'default' => 4096, 'save_method' => 'storeSettingField' ), - 'system_letsencryptreuseold' => array( - 'label' => $lng['serversettings']['letsencryptreuseold'], + 'system_leecc' => array( + 'label' => $lng['serversettings']['letsencryptecc'], 'settinggroup' => 'system', - 'varname' => 'letsencryptreuseold', - 'type' => 'bool', - 'default' => false, + 'varname' => 'leecc', + 'type' => 'option', + 'default' => '0', + 'option_mode' => 'one', + 'option_options' => array( + '0' => '-', + '256' => 'ec-256', + '384' => 'ec-384' + ), 'save_method' => 'storeSettingField' ), 'system_disable_le_selfcheck' => array( diff --git a/install/froxlor.sql b/install/froxlor.sql index ae458ea2..840fd8a2 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -644,6 +644,7 @@ opcache.interned_strings_buffer'), ('system', 'logfiles_script', ''), ('system', 'dhparams_file', ''), ('system', 'errorlog_level', 'warn'), + ('system', 'leecc', '0'), ('api', 'enabled', '0'), ('2fa', 'enabled', '1'), ('panel', 'decimal_places', '4'), @@ -679,7 +680,7 @@ opcache.interned_strings_buffer'), ('panel', 'customer_hide_options', ''), ('panel', 'is_configured', '0'), ('panel', 'version', '0.10.0'), - ('panel', 'db_version', '201902120'); + ('panel', 'db_version', '201902170'); DROP TABLE IF EXISTS `panel_tasks`; diff --git a/install/updates/froxlor/0.10/update_0.10.inc.php b/install/updates/froxlor/0.10/update_0.10.inc.php index 17cab095..93a21f61 100644 --- a/install/updates/froxlor/0.10/update_0.10.inc.php +++ b/install/updates/froxlor/0.10/update_0.10.inc.php @@ -196,3 +196,24 @@ if (\Froxlor\Froxlor::isDatabaseVersion('201812190')) { \Froxlor\Froxlor::updateToDbVersion('201902120'); } + +if (\Froxlor\Froxlor::isDatabaseVersion('201902120')) { + + showUpdateStep("Adding new ECC / ECDSA setting for Let's Encrypt"); + Settings::AddNew('system.leecc', '0'); + Database::query("UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `cronclass` = '\\Froxlor\\Cron\\Http\\LetsEncrypt\\AcmeSh' WHERE `cronfile` = 'letsencrypt'"); + lastStepStatus(0); + + showUpdateStep("Removing current Let's Encrypt certificates due to new implementation of acme.sh"); + $sel_result = Database::query("SELECT id FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `letsencrypt` = '1'"); + $domain_ids = $sel_result->fetchAll(\PDO::FETCH_ASSOC); + $domain_in = ""; + foreach ($domain_ids as $domain_id) { + $domain_in .= "'".$domain_id['id']."',"; + } + $domain_in = substr($domain_in, 0, -1); + Database::query("DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` IN (". $domain_in . ")"); + lastStepStatus(0); + + \Froxlor\Froxlor::updateToDbVersion('201902170'); +} diff --git a/lib/Froxlor/Cron/Http/LetsEncrypt/LetsEncrypt.php b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php similarity index 53% rename from lib/Froxlor/Cron/Http/LetsEncrypt/LetsEncrypt.php rename to lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php index 298c920d..10427b02 100644 --- a/lib/Froxlor/Cron/Http/LetsEncrypt/LetsEncrypt.php +++ b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php @@ -1,9 +1,9 @@ logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating Let's Encrypt certificates"); - if (! extension_loaded('curl')) { - FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Let's Encrypt requires the php cURL extension to be installed."); - exit(); - } - $certificates_stmt = Database::query(" - SELECT - domssl.`id`, - domssl.`domainid`, - domssl.expirationdate, - domssl.`ssl_cert_file`, - domssl.`ssl_key_file`, - domssl.`ssl_ca_file`, - domssl.`ssl_csr_file`, - dom.`domain`, - dom.`wwwserveralias`, - dom.`documentroot`, - dom.`id` AS 'domainid', - dom.`ssl_redirect`, - cust.`leprivatekey`, - cust.`lepublickey`, - cust.`leregistered`, - cust.`customerid`, - cust.`loginname` - FROM - `" . TABLE_PANEL_CUSTOMERS . "` AS cust, - `" . TABLE_PANEL_DOMAINS . "` AS dom - LEFT JOIN - `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON - dom.`id` = domssl.`domainid` - WHERE - dom.`customerid` = cust.`customerid` - AND cust.deactivated = 0 - AND dom.`letsencrypt` = 1 - AND dom.`aliasdomain` IS NULL - AND dom.`iswildcarddomain` = 0 - AND ( - domssl.`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) - OR domssl.`expirationdate` IS NULL - ) - "); + SELECT + domssl.`id`, + domssl.`domainid`, + domssl.expirationdate, + domssl.`ssl_cert_file`, + domssl.`ssl_key_file`, + domssl.`ssl_ca_file`, + domssl.`ssl_csr_file`, + dom.`domain`, + dom.`wwwserveralias`, + dom.`documentroot`, + dom.`id` AS 'domainid', + dom.`ssl_redirect`, + cust.`leprivatekey`, + cust.`lepublickey`, + cust.`leregistered`, + cust.`customerid`, + cust.`loginname` + FROM + `" . TABLE_PANEL_CUSTOMERS . "` AS cust, + `" . TABLE_PANEL_DOMAINS . "` AS dom + LEFT JOIN + `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON + dom.`id` = domssl.`domainid` + WHERE + dom.`customerid` = cust.`customerid` + AND cust.deactivated = 0 + AND dom.`letsencrypt` = 1 + AND dom.`aliasdomain` IS NULL + AND dom.`iswildcarddomain` = 0 + AND ( + domssl.`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) + OR domssl.`expirationdate` IS NULL + ) + "); $aliasdomains_stmt = Database::prepare(" - SELECT - dom.`id` as domainid, - dom.`domain`, - dom.`wwwserveralias` - FROM `" . TABLE_PANEL_DOMAINS . "` AS dom - WHERE - dom.`aliasdomain` = :id - AND dom.`letsencrypt` = 1 - AND dom.`iswildcarddomain` = 0 - "); + SELECT + dom.`id` as domainid, + dom.`domain`, + dom.`wwwserveralias` + FROM `" . TABLE_PANEL_DOMAINS . "` AS dom + WHERE + dom.`aliasdomain` = :id + AND dom.`letsencrypt` = 1 + AND dom.`iswildcarddomain` = 0 + "); $updcert_stmt = Database::prepare(" - REPLACE INTO - `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` - SET - `id` = :id, - `domainid` = :domainid, - `ssl_cert_file` = :crt, - `ssl_key_file` = :key, - `ssl_ca_file` = :ca, - `ssl_cert_chainfile` = :chain, - `ssl_csr_file` = :csr, - `ssl_fullchain_file` = :fullchain, - `expirationdate` = :expirationdate - "); + REPLACE INTO + `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` + SET + `id` = :id, + `domainid` = :domainid, + `ssl_cert_file` = :crt, + `ssl_key_file` = :key, + `ssl_ca_file` = :ca, + `ssl_cert_chainfile` = :chain, + `ssl_csr_file` = :csr, + `ssl_fullchain_file` = :fullchain, + `expirationdate` = :expirationdate + "); $upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid"); @@ -130,14 +127,15 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron ); $froxlor_ssl_settings_stmt = Database::prepare(" - SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` - WHERE `domainid` = '0' AND - (`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL) - "); + SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` + WHERE `domainid` = '0' AND + (`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL) + "); $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); - $insert_or_update_required = true; + $cert_mode = 'issue'; if ($froxlor_ssl) { + $cert_mode = 'renew'; $certrow['id'] = $froxlor_ssl['id']; $certrow['expirationdate'] = $froxlor_ssl['expirationdate']; $certrow['ssl_cert_file'] = $froxlor_ssl['ssl_cert_file']; @@ -148,38 +146,51 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron // check whether we have an entry with valid certificates which just does not need // updating yet, so we need to skip this here $froxlor_ssl_settings_stmt = Database::prepare(" - SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' - "); + SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' + "); $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); if ($froxlor_ssl && ! empty($froxlor_ssl['ssl_cert_file'])) { - $insert_or_update_required = false; + $cert_mode = false; } } - if ($insert_or_update_required) { + if ($cert_mode) { $domains = array( $certrow['domain'] ); // Only renew let's encrypt certificate if no broken ssl_redirect is enabled // - this temp. deactivation of the ssl-redirect is handled by the webserver-cronjob - FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']); + if ($cert_mode == 'renew') { + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']); + } else { + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating certificate for " . $certrow['domain']); + } $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => $certrow['loginname'] + 'loginname' => $certrow['loginname'], + 'adminsession' => 0 )); - try { - // Initialize Lescript with documentroot - $le = new \Froxlor\Http\LetsEncrypt\LeScript($cronlog, \Froxlor\Froxlor::getVersion()); + // Initialize Lescript with documentroot + $acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --" . $cert_mode . " -d " . implode(" -d ", $domains); - // Initialize Lescript - $le->initAccount($certrow, true); + if ($cert_mode == 'issue') { + $acmesh_cmd .= " -w " . \Froxlor\Froxlor::getInstallDir(); + } + if (Settings::Get('system.leecc') > 0) { + $acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc'); + } + + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, $acmesh_cmd); - // Request the new certificate (old key may be used) - $return = $le->signDomains($domains, $certrow['ssl_key_file']); + $acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd); + + $return = array(); + self::readCertificateToVar(array_pop($domains), $return); + + if (! empty($return['crt'])) { - // We are interessted in the expirationdate $newcert = openssl_x509_parse($return['crt']); // Store the new data @@ -198,28 +209,34 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron if ($certrow['ssl_redirect'] == 3) { Settings::Set('system.le_froxlor_redirect', '1'); } - - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); - + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); $changedetected = 1; - } catch (\Exception $e) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage()); + } else { + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result)); } } } // customer domains $certrows = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC); + $cert_mode = 'issue'; foreach ($certrows as $certrow) { // set logger to corresponding loginname for the log to appear in the users system-log $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => $certrow['loginname'] + 'loginname' => $certrow['loginname'], + 'adminsession' => 0 )); // Only renew let's encrypt certificate if no broken ssl_redirect is enabled if ($certrow['ssl_redirect'] != 2) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']); + + if (!empty($certrow['ssl_cert_file'])) { + $cert_mode = 'renew'; + $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating certificate for " . $certrow['domain']); + } else { + $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']); + } $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $certrow['domain']); $domains = array( @@ -245,17 +262,23 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron } } - try { - // Initialize Lescript with documentroot - $le = new \Froxlor\Http\LetsEncrypt\LeScript($cronlog, \Froxlor\Froxlor::getVersion()); + // Initialize Lescript with documentroot + $acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --" . $cert_mode . " -d " . implode(" -d ", $domains); - // Initialize Lescript - $le->initAccount($certrow); + if ($cert_mode == 'issue') { + $acmesh_cmd .= " -w " . \Froxlor\Froxlor::getInstallDir(); + } + if (Settings::Get('system.leecc') > 0) { + $acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc'); + } - // Request the new certificate (old key may be used) - $return = $le->signDomains($domains, $certrow['ssl_key_file']); + $acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd); + + $return = array(); + self::readCertificateToVar($certrow['domain'], $return); + + if (! empty($return['crt'])) { - // We are interessted in the expirationdate $newcert = openssl_x509_parse($return['crt']); // Store the new data @@ -278,10 +301,9 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron } $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); - $changedetected = 1; - } catch (\Exception $e) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage()); + } else { + $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result)); } } else { $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect"); @@ -294,10 +316,33 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron \Froxlor\System\Cronjob::inserttask(1); } - // reset logger - $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => 'cronjob' - )); - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated"); + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated"); + } + + private static function readCertificateToVar($domain, &$return) + { + $certificate_folder = dirname(self::$acmesh) . "/" . $domain; + if (Settings::Get('system.leecc') > 0) { + $certificate_folder .= "_ecc"; + } + $certificate_folder = \Froxlor\FileDir::makeCorrectDir($certificate_folder); + + if (is_dir($certificate_folder)) { + $return['crt'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.cer')); + $return['key'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.key')); + $return['chain'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/ca.cer')); + $return['fullchain'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer')); + $return['csr'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.csr')); + } + } + + private static function checkInstall() + { + if (! file_exists(self::$acmesh)) { + FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Could not find acme.sh - installing it to /root/.acme.sh/"); + \Froxlor\FileDir::safe_exec("wget -O - https://get.acme.sh | sh", false, array( + '|' + )); + } } } diff --git a/lib/Froxlor/Cron/Http/LetsEncrypt/LetsEncryptV2.php b/lib/Froxlor/Cron/Http/LetsEncrypt/LetsEncryptV2.php deleted file mode 100644 index bff5b3c1..00000000 --- a/lib/Froxlor/Cron/Http/LetsEncrypt/LetsEncryptV2.php +++ /dev/null @@ -1,307 +0,0 @@ - - * @author Froxlor team (2016-) - * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt - * @package Cron - * - * @since 0.9.35 - * - */ -class LetsEncryptV2 extends \Froxlor\Cron\FroxlorCron -{ - - public static function run() - { - FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating Let's Encrypt certificates"); - - if (! extension_loaded('curl')) { - FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Let's Encrypt requires the php cURL extension to be installed."); - exit(); - } - - $certificates_stmt = Database::query(" - SELECT - domssl.`id`, - domssl.`domainid`, - domssl.expirationdate, - domssl.`ssl_cert_file`, - domssl.`ssl_key_file`, - domssl.`ssl_ca_file`, - domssl.`ssl_csr_file`, - dom.`domain`, - dom.`wwwserveralias`, - dom.`iswildcarddomain`, - dom.`documentroot`, - dom.`id` AS 'domainid', - dom.`ssl_redirect`, - cust.`leprivatekey`, - cust.`lepublickey`, - cust.`leregistered`, - cust.`leaccount`, - cust.`customerid`, - cust.`loginname` - FROM - `" . TABLE_PANEL_CUSTOMERS . "` AS cust, - `" . TABLE_PANEL_DOMAINS . "` AS dom - LEFT JOIN - `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON - dom.`id` = domssl.`domainid` - WHERE - dom.`customerid` = cust.`customerid` - AND cust.deactivated = 0 - AND dom.`letsencrypt` = 1 - AND dom.`aliasdomain` IS NULL - AND dom.`iswildcarddomain` = 0 - AND ( - domssl.`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) - OR domssl.`expirationdate` IS NULL - ) - "); - - $aliasdomains_stmt = Database::prepare(" - SELECT - dom.`id` as domainid, - dom.`domain`, - dom.`wwwserveralias`, - dom.`iswildcarddomain` - FROM `" . TABLE_PANEL_DOMAINS . "` AS dom - WHERE - dom.`aliasdomain` = :id - AND dom.`letsencrypt` = 1 - AND dom.`iswildcarddomain` = 0 - "); - - $updcert_stmt = Database::prepare(" - REPLACE INTO - `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` - SET - `id` = :id, - `domainid` = :domainid, - `ssl_cert_file` = :crt, - `ssl_key_file` = :key, - `ssl_ca_file` = :ca, - `ssl_cert_chainfile` = :chain, - `ssl_csr_file` = :csr, - `ssl_fullchain_file` = :fullchain, - `expirationdate` = :expirationdate - "); - - $upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid"); - - // flag for re-generation of vhost files - $changedetected = 0; - - // first - generate LE for system-vhost if enabled - if (Settings::Get('system.le_froxlor_enabled') == '1') { - - $certrow = array( - 'loginname' => 'froxlor.panel', - 'domain' => Settings::Get('system.hostname'), - 'domainid' => 0, - 'documentroot' => \Froxlor\Froxlor::getInstallDir(), - 'leprivatekey' => Settings::Get('system.leprivatekey'), - 'lepublickey' => Settings::Get('system.lepublickey'), - 'leregistered' => Settings::Get('system.leregistered'), - 'leaccount' => Settings::Get('system.leaccount'), - 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'), - 'expirationdate' => null, - 'ssl_cert_file' => null, - 'ssl_key_file' => null, - 'ssl_ca_file' => null, - 'ssl_csr_file' => null, - 'id' => null - ); - - $froxlor_ssl_settings_stmt = Database::prepare(" - SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` - WHERE `domainid` = '0' AND - (`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL) - "); - $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); - - $insert_or_update_required = true; - if ($froxlor_ssl) { - $certrow['id'] = $froxlor_ssl['id']; - $certrow['expirationdate'] = $froxlor_ssl['expirationdate']; - $certrow['ssl_cert_file'] = $froxlor_ssl['ssl_cert_file']; - $certrow['ssl_key_file'] = $froxlor_ssl['ssl_key_file']; - $certrow['ssl_ca_file'] = $froxlor_ssl['ssl_ca_file']; - $certrow['ssl_csr_file'] = $froxlor_ssl['ssl_csr_file']; - } else { - // check whether we have an entry with valid certificates which just does not need - // updating yet, so we need to skip this here - $froxlor_ssl_settings_stmt = Database::prepare(" - SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' - "); - $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); - if ($froxlor_ssl && ! empty($froxlor_ssl['ssl_cert_file'])) { - $insert_or_update_required = false; - } - } - - if ($insert_or_update_required) { - $domains = array( - $certrow['domain'] - ); - - // Only renew let's encrypt certificate if no broken ssl_redirect is enabled - // - this temp. deactivation of the ssl-redirect is handled by the webserver-cronjob - FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']); - - $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => $certrow['loginname'] - )); - - try { - // Initialize Lescript with documentroot - $le = new \Froxlor\Http\LetsEncrypt\LeScriptV2($cronlog, \Froxlor\Froxlor::getVersion()); - - // Initialize Lescript - $le->initAccount($certrow, true); - - // Request the new certificate (old key may be used) - $return = $le->signDomains($domains, $certrow['ssl_key_file']); - - // We are interessted in the expirationdate - $newcert = openssl_x509_parse($return['crt']); - - // Store the new data - Database::pexecute($updcert_stmt, array( - 'id' => $certrow['id'], - 'domainid' => $certrow['domainid'], - 'crt' => $return['crt'], - 'key' => $return['key'], - 'ca' => $return['chain'], - 'chain' => $return['chain'], - 'csr' => $return['csr'], - 'fullchain' => $return['fullchain'], - 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) - )); - - if ($certrow['ssl_redirect'] == 3) { - Settings::Set('system.le_froxlor_redirect', '1'); - } - - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); - - $changedetected = 1; - } catch (\Exception $e) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage()); - } - } - } - - // customer domains - $certrows = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC); - foreach ($certrows as $certrow) { - - // set logger to corresponding loginname for the log to appear in the users system-log - $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => $certrow['loginname'] - )); - - // Only renew let's encrypt certificate if no broken ssl_redirect is enabled - if ($certrow['ssl_redirect'] != 2) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']); - - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $certrow['domain']); - $domains = array( - $certrow['domain'] - ); - if ($certrow['iswildcarddomain'] == 1) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: *." . $certrow['domain']); - $domains[] = '*.' . $certrow['domain']; - } elseif ($certrow['wwwserveralias'] == 1) { - // add www. to SAN list - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $certrow['domain']); - $domains[] = 'www.' . $certrow['domain']; - } - - // add alias domains (and possibly www.) to SAN list - Database::pexecute($aliasdomains_stmt, array( - 'id' => $certrow['domainid'] - )); - $aliasdomains = $aliasdomains_stmt->fetchAll(\PDO::FETCH_ASSOC); - foreach ($aliasdomains as $aliasdomain) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $aliasdomain['domain']); - $domains[] = $aliasdomain['domain']; - if ($aliasdomain['iswildcarddomain'] == 1) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: *." . $aliasdomain['domain']); - $domains[] = '*.' . $aliasdomain['domain']; - } elseif ($aliasdomain['wwwserveralias'] == 1) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $aliasdomain['domain']); - $domains[] = 'www.' . $aliasdomain['domain']; - } - } - - try { - // Initialize Lescript with documentroot - $le = new \Froxlor\Http\LetsEncrypt\LeScriptV2($cronlog, \Froxlor\Froxlor::getVersion()); - - // Initialize Lescript - $le->initAccount($certrow); - - // Request the new certificate (old key may be used) - $return = $le->signDomains($domains, $certrow['ssl_key_file']); - - // We are interessted in the expirationdate - $newcert = openssl_x509_parse($return['crt']); - - // Store the new data - Database::pexecute($updcert_stmt, array( - 'id' => $certrow['id'], - 'domainid' => $certrow['domainid'], - 'crt' => $return['crt'], - 'key' => $return['key'], - 'ca' => $return['chain'], - 'chain' => $return['chain'], - 'csr' => $return['csr'], - 'fullchain' => $return['fullchain'], - 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) - )); - - if ($certrow['ssl_redirect'] == 3) { - Database::pexecute($upddom_stmt, array( - 'domainid' => $certrow['domainid'] - )); - } - - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); - - $changedetected = 1; - } catch (\Exception $e) { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage()); - } - } else { - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect"); - } - } - - // If we have a change in a certificate, we need to update the webserver - configs - // This is easiest done by just creating a new task ;) - if ($changedetected) { - \Froxlor\System\Cronjob::inserttask(1); - } - - // reset logger - $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => 'cronjob' - )); - $cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated"); - } -} diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php index 3311a958..7e0bb70d 100644 --- a/lib/Froxlor/Froxlor.php +++ b/lib/Froxlor/Froxlor.php @@ -10,7 +10,7 @@ final class Froxlor const VERSION = '0.10.0'; // Database version (YYYYMMDDC where C is a daily counter) - const DBVERSION = '201902120'; + const DBVERSION = '201902170'; // Distribution branding-tag (used for Debian etc.) const BRANDING = ''; diff --git a/lib/Froxlor/Http/LetsEncrypt/Base64UrlSafeEncoder.php b/lib/Froxlor/Http/LetsEncrypt/Base64UrlSafeEncoder.php deleted file mode 100644 index 4f3fa7c2..00000000 --- a/lib/Froxlor/Http/LetsEncrypt/Base64UrlSafeEncoder.php +++ /dev/null @@ -1,21 +0,0 @@ -base = $base; - } - - private function curl($method, $url, $data = null) - { - $headers = array( - 'Accept: application/json', - 'Content-Type: application/json' - ); - $handle = curl_init(); - curl_setopt($handle, CURLOPT_URL, preg_match('~^http~', $url) ? $url : $this->base . $url); - curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); - curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); - curl_setopt($handle, CURLOPT_HEADER, true); - - // DO NOT DO THAT! - // curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false); - // curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false); - - switch ($method) { - case 'GET': - break; - case 'POST': - curl_setopt($handle, CURLOPT_POST, true); - curl_setopt($handle, CURLOPT_POSTFIELDS, $data); - break; - } - $response = curl_exec($handle); - - if (curl_errno($handle)) { - throw new \RuntimeException('Curl: ' . curl_error($handle)); - } - - $header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE); - - $header = substr($response, 0, $header_size); - $body = substr($response, $header_size); - - $this->lastHeader = $header; - $this->lastCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); - - $data = json_decode($body, true); - return $data === null ? $body : $data; - } - - public function post($url, $data) - { - return $this->curl('POST', $url, $data); - } - - public function get($url) - { - return $this->curl('GET', $url); - } - - public function getLastNonce() - { - if (preg_match('~Replay\-Nonce: (.+)~i', $this->lastHeader, $matches)) { - return trim($matches[1]); - } - - $this->curl('GET', '/directory'); - return $this->getLastNonce(); - } - - public function getLastLocation() - { - if (preg_match('~Location: (.+)~i', $this->lastHeader, $matches)) { - return trim($matches[1]); - } - return null; - } - - public function getLastCode() - { - return $this->lastCode; - } - - public function getLastLinks() - { - preg_match_all('~Link: <(.+)>;rel="up"~', $this->lastHeader, $matches); - return $matches[1]; - } -} diff --git a/lib/Froxlor/Http/LetsEncrypt/LeScript.php b/lib/Froxlor/Http/LetsEncrypt/LeScript.php deleted file mode 100644 index 7e4a8d5f..00000000 --- a/lib/Froxlor/Http/LetsEncrypt/LeScript.php +++ /dev/null @@ -1,481 +0,0 @@ - -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// * Neither the name of the nor the -// names of its contributors may be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// This file is copied from https://github.com/analogic/lescript -// and modified to work without files and integrate in Froxlor -class LeScript -{ - - // https://letsencrypt.org/repository/ - private $logger; - - private $client; - - private $accountKey; - - private $customerid; - - private $isFroxlorVhost; - - private $isLeProduction; - - private $version; - - public function __construct($logger, $version = '1') - { - $this->logger = $logger; - $this->version = $version; - if (Settings::Get('system.letsencryptca') == 'production') { - $ca = 'https://acme-v01.api.letsencrypt.org'; - } else { - $ca = 'https://acme-staging.api.letsencrypt.org'; - } - $this->client = new Client($ca); - $this->log("Using '$ca' to generate certificate"); - } - - public function initAccount($certrow, $isFroxlorVhost = false) - { - // Let's see if we have the private accountkey - $this->accountKey = $certrow['leprivatekey']; - $this->customerId = (! $isFroxlorVhost ? $certrow['customerid'] : null); - $this->isFroxlorVhost = $isFroxlorVhost; - $this->isLeProduction = (Settings::Get('system.letsencryptca') == 'production'); - - $leregistered = $certrow['leregistered']; - - if (! $this->accountKey || $this->accountKey == 'unset' || ! $this->isLeProduction) { - - // generate and save new private key for account - // --------------------------------------------- - - $this->log('Creating new account key'); - $keys = $this->generateKey(); - // Only store the accountkey in production, in staging always generate a new key - if ($this->isLeProduction) { - if ($isFroxlorVhost) { - Settings::Set('system.lepublickey', $keys['public']); - Settings::Set('system.leprivatekey', $keys['private']); - Settings::Set('system.leregistered', 0); // key is not registered - } else { - $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private, `leregistered` = :registered WHERE `customerid` = :customerid;"); - Database::pexecute($upd_stmt, array( - 'public' => $keys['public'], - 'private' => $keys['private'], - 'registered' => 0, - 'customerid' => $this->customerId - )); - } - } - $leregistered = 0; - $this->accountKey = $keys['private']; - } else { - $this->log('Using existing account key'); - } - - if ($leregistered == 0) { // Account not registered - - $this->log('Starting new account registration'); - $response = $this->postNewReg(); - if ($this->client->getLastCode() == 409) { - $this->log('The key was already registered. Using existing account.'); - } elseif ($this->client->getLastCode() == 201) { - $this->log('New account registered.'); - } else { - throw new \RuntimeException("Account not initialized, probably due to rate limiting. Whole response: " . json_encode($response)); - } - $accountUrl = $this->client->getLastLocation(); - - $leregistered = 1; - $this->setLeRegisteredState($leregistered); // Account registered - $this->log('Lets encrypt Terms of Service accepted'); - } - } - - /** - * - * @param array $domains - * @param string $domainkey - * @param string $csr - * optional, same behavior as $reuseCsr from the original class, but we're passing the content of the csr already - * - * @throws \RuntimeException - * @return string[] - */ - public function signDomains(array $domains, $domainkey = null, $csr = null) - { - if (! $this->accountKey) { - throw new \RuntimeException("Account not initialized"); - } - - $this->log('Starting certificate generation process for domains'); - - $privateAccountKey = openssl_pkey_get_private($this->accountKey); - $accountKeyDetails = openssl_pkey_get_details($privateAccountKey); - - // start domains authentication - // ---------------------------- - - foreach ($domains as $domain) { - - // 1. getting available authentication options - // ------------------------------------------- - - $this->log("Requesting challenge for $domain"); - - $response = $this->signedRequest("/acme/new-authz", array( - "resource" => "new-authz", - "identifier" => array( - "type" => "dns", - "value" => $domain - ) - )); - - if ($this->client->getLastCode() == 403) { - $this->log("Got status 403 - setting LE status to unregistered."); - $this->setLeRegisteredState(0); - throw new \RuntimeException("Got 'unauthorized' response - we need to re-register at next run. Whole response: " . json_encode($response)); - } - - // if response is not an array but a string, it's most likely a server-error, e.g. - // ErrorAn error occurred while processing your request. - //

Reference #179.d8be1402.1458059103.3613c4db - if (! is_array($response)) { - throw new \RuntimeException("Invalid response from LE for domain $domain. Whole response: " . json_encode($response)); - } - - if (! array_key_exists('challenges', $response)) { - throw new \RuntimeException("No challenges received for $domain. Whole response: " . json_encode($response)); - } - - // choose http-01 challenge only - $challenge = array_reduce($response['challenges'], function ($v, $w) { - return $v ? $v : ($w['type'] == 'http-01' ? $w : false); - }); - - if (! $challenge) { - throw new \RuntimeException("HTTP Challenge for $domain is not available. Whole response: " . json_encode($response)); - } - - $this->log("Got challenge token for $domain"); - $location = $this->client->getLastLocation(); - - // 2. saving authentication token for web verification - // --------------------------------------------------- - - $directory = Settings::Get('system.letsencryptchallengepath') . '/.well-known/acme-challenge'; - $tokenPath = $directory . '/' . $challenge['token']; - - if (! file_exists($directory) && ! @mkdir($directory, 0755, true)) { - throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); - } - - $header = array( - // need to be in precise order! - "e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]), - "kty" => "RSA", - "n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"]) - ); - $payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true)); - - file_put_contents($tokenPath, $payload); - chmod($tokenPath, 0644); - - // 3. verification process itself - // ------------------------------- - - $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - - $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); - - // simple self check - if (Settings::Get('system.disable_le_selfcheck') == '0') { - $selfcheckpayload = \Froxlor\Http\HttpClient::urlGet($uri, false); - if ($payload !== trim($selfcheckpayload)) { - $errmsg = json_encode(error_get_last()); - if ($errmsg != "null") { - $errmsg = "; PHP error: " . $errmsg; - } else { - $errmsg = ""; - } - $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "[Lets Encrypt self-check] Please check $uri - token seems to be not available. This is just a simple self-check, it might be wrong but consider using this information when Let's Encrypt fails to issue a certificate" . $errmsg); - } - } - - $this->log("Sending request to challenge"); - - // send request to challenge - $result = $this->signedRequest($challenge['uri'], array( - "resource" => "challenge", - "type" => "http-01", - "keyAuthorization" => $payload, - "token" => $challenge['token'] - )); - - // waiting loop - // we wait for a maximum of 30 seconds to avoid endless loops - $count = 0; - do { - if (empty($result['status']) || $result['status'] == "invalid") { - @unlink($tokenPath); - throw new \RuntimeException("Verification ended with error: " . json_encode($result)); - } - $ended = ! ($result['status'] === "pending"); - - if (! $ended) { - $this->log("Verification pending, sleeping 1s"); - sleep(1); - $count ++; - } - - $result = $this->client->get($location); - } while (! $ended && $count < 30); - - $this->log("Verification ended with status: ${result['status']}"); - @unlink($tokenPath); - } - - // requesting certificate - // ---------------------- - - // generate private key for domain if not exist - if (empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) { - $keys = $this->generateKey(); - $domainkey = $keys['private']; - } - - // load domain key - $privateDomainKey = openssl_pkey_get_private($domainkey); - - $this->client->getLastLinks(); - - if (empty($csr)) { - $csr = $this->generateCSR($privateDomainKey, $domains); - } - - // request certificates creation - $result = $this->signedRequest("/acme/new-cert", array( - 'resource' => 'new-cert', - 'csr' => $csr - )); - if ($this->client->getLastCode() !== 201) { - throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result)); - } - $location = $this->client->getLastLocation(); - - // waiting loop - $certificates = array(); - while (1) { - $this->client->getLastLinks(); - - $result = $this->client->get($location); - - if ($this->client->getLastCode() == 202) { - - $this->log("Certificate generation pending, sleeping 1s"); - sleep(1); - } elseif ($this->client->getLastCode() == 200) { - - $this->log("Got certificate! YAY!"); - $certificates[] = $this->parsePemFromBody($result); - - foreach ($this->client->getLastLinks() as $link) { - $this->log("Requesting chained cert at $link"); - $result = $this->client->get($link); - $certificates[] = $this->parsePemFromBody($result); - } - - break; - } else { - - throw new \RuntimeException("Can't get certificate: HTTP code " . $this->client->getLastCode()); - } - } - - if (empty($certificates)) - throw new \RuntimeException('No certificates generated'); - - $fullchain = implode("\n", $certificates); - $crt = array_shift($certificates); - $chain = implode("\n", $certificates); - - $this->log("Done, returning new certificates and key"); - return array( - 'fullchain' => $fullchain, - 'crt' => $crt, - 'chain' => $chain, - 'key' => $domainkey, - 'csr' => $csr - ); - } - - private function setLeRegisteredState($state) - { - if ($this->isLeProduction) { - if ($this->isFroxlorVhost) { - Settings::Set('system.leregistered', $state); - } else { - $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `leregistered` = :registered WHERE `customerid` = :customerid;"); - Database::pexecute($upd_stmt, array( - 'registered' => $state, - 'customerid' => $this->customerId - )); - } - } - } - - private function parsePemFromBody($body) - { - $pem = chunk_split(base64_encode($body), 64, "\n"); - return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; - } - - private function postNewReg() - { - $this->log('Getting last terms of service URL'); - $directory = $this->client->get('/directory'); - if (! isset($directory['meta']) || ! isset($directory['meta']['terms-of-service'])) { - throw new \RuntimeException("No terms of service link available!"); - } - $this->log('Sending registration to letsencrypt server'); - - return $this->signedRequest('/acme/new-reg', array( - 'resource' => 'new-reg', - 'agreement' => $directory['meta']['terms-of-service'] - )); - } - - private function generateCSR($privateKey, array $domains) - { - $domain = reset($domains); - $san = implode(",", array_map(function ($dns) { - return "DNS:" . $dns; - }, $domains)); - $tmpConf = tmpfile(); - $tmpConfMeta = stream_get_meta_data($tmpConf); - $tmpConfPath = $tmpConfMeta["uri"]; - - // workaround to get SAN working - fwrite($tmpConf, 'HOME = . -RANDFILE = $ENV::HOME/.rnd -[ req ] -default_bits = ' . Settings::Get('system.letsencryptkeysize') . ' -default_keyfile = privkey.pem -distinguished_name = req_distinguished_name -req_extensions = v3_req -[ req_distinguished_name ] -countryName = Country Name (2 letter code) -[ v3_req ] -basicConstraints = CA:FALSE -subjectAltName = ' . $san . ' -keyUsage = nonRepudiation, digitalSignature, keyEncipherment'); - - $csr = openssl_csr_new(array( - "CN" => $domain, - "ST" => Settings::Get('system.letsencryptstate'), - "C" => Settings::Get('system.letsencryptcountrycode'), - "O" => "Unknown" - ), $privateKey, array( - "config" => $tmpConfPath, - "digest_alg" => "sha256" - )); - - if (! $csr) - throw new \RuntimeException("CSR couldn't be generated! " . openssl_error_string()); - - openssl_csr_export($csr, $csr); - fclose($tmpConf); - - preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); - - return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); - } - - private function generateKey() - { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_RSA, - "private_key_bits" => (int) Settings::Get('system.letsencryptkeysize') - )); - - if (! openssl_pkey_export($res, $privateKey)) { - throw new \RuntimeException("Key export failed!"); - } - - $details = openssl_pkey_get_details($res); - - return array( - 'private' => $privateKey, - 'public' => $details['key'] - ); - } - - private function signedRequest($uri, array $payload) - { - $privateKey = openssl_pkey_get_private($this->accountKey); - $details = openssl_pkey_get_details($privateKey); - - $header = array( - "alg" => "RS256", - "jwk" => array( - "kty" => "RSA", - "n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]), - "e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"]) - ) - ); - - $protected = $header; - $protected["nonce"] = $this->client->getLastNonce(); - - $payload64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload))); - $protected64 = Base64UrlSafeEncoder::encode(json_encode($protected)); - - openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256"); - - $signed64 = Base64UrlSafeEncoder::encode($signed); - - $data = array( - 'header' => $header, - 'protected' => $protected64, - 'payload' => $payload64, - 'signature' => $signed64 - ); - - $this->log("Sending signed request to $uri"); - - return $this->client->post($uri, json_encode($data)); - } - - protected function log($message) - { - $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "letsencrypt " . $message); - } -} diff --git a/lib/Froxlor/Http/LetsEncrypt/LeScriptV2.php b/lib/Froxlor/Http/LetsEncrypt/LeScriptV2.php deleted file mode 100644 index e0c04cba..00000000 --- a/lib/Froxlor/Http/LetsEncrypt/LeScriptV2.php +++ /dev/null @@ -1,498 +0,0 @@ - -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// * Neither the name of the nor the -// names of its contributors may be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// This file is copied from https://github.com/analogic/lescript -// and modified to work without files and integrate in Froxlor -class LeScriptV2 -{ - - // https://letsencrypt.org/repository/ - private $logger; - - private $client; - - private $accountKey; - - private $customerid; - - private $isFroxlorVhost; - - private $isLeProduction; - - private $version; - - private $req_uris = array(); - - private $acc_location = null; - - public function __construct($logger, $version = '2') - { - $this->logger = $logger; - $this->version = $version; - if (Settings::Get('system.letsencryptca') == 'production') { - $ca = 'https://acme-v02.api.letsencrypt.org'; - } else { - $ca = 'https://acme-staging-v02.api.letsencrypt.org'; - } - $this->client = new Client($ca); - $this->log("Using '$ca' to generate certificate"); - - // get request-uris from /directory - $response = $this->client->get('/directory'); - $this->req_uris['newAccount'] = $response['newAccount']; - $this->req_uris['newOrder'] = $response['newOrder']; - $this->req_uris['newNonce'] = $response['newNonce']; - $this->req_uris['revokeCert'] = $response['revokeCert']; - } - - public function initAccount($certrow, $isFroxlorVhost = false) - { - // Let's see if we have the private accountkey - $this->accountKey = $certrow['leprivatekey']; - $this->customerId = (! $isFroxlorVhost ? $certrow['customerid'] : null); - $this->isFroxlorVhost = $isFroxlorVhost; - $this->isLeProduction = (Settings::Get('system.letsencryptca') == 'production'); - $this->acc_location = $certrow['leaccount']; - - $leregistered = $certrow['leregistered']; - - if (! $this->accountKey || $this->accountKey == 'unset' || ! $this->isLeProduction) { - - // generate and save new private key for account - // --------------------------------------------- - - $this->log('Creating new account key'); - $keys = $this->generateKey(); - // Only store the accountkey in production, in staging always generate a new key - if ($this->isLeProduction) { - if ($isFroxlorVhost) { - Settings::Set('system.lepublickey', $keys['public']); - Settings::Set('system.leprivatekey', $keys['private']); - Settings::Set('system.leregistered', 0); // key is not registered - } else { - $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private, `leregistered` = :registered WHERE `customerid` = :customerid;"); - Database::pexecute($upd_stmt, array( - 'public' => $keys['public'], - 'private' => $keys['private'], - 'registered' => 0, - 'customerid' => $this->customerId - )); - } - } - $leregistered = 0; - $this->accountKey = $keys['private']; - } else { - $this->log('Using existing account key'); - } - - if ($leregistered == 0) { // Account not registered - - $this->log('Starting new account registration'); - $response = $this->postNewReg(); - if ($this->client->getLastCode() == 409) { - $this->log('The key was already registered. Using existing account.'); - } elseif ($this->client->getLastCode() == 201) { - $this->log('New account registered.'); - } else { - throw new \RuntimeException("Account not initialized, probably due to rate limiting. Whole response: " . json_encode($response)); - } - $this->acc_location = $this->client->getLastLocation(); - - $leregistered = 1; - $this->setLeRegisteredState($leregistered); - } - } - - /** - * - * @param array $domains - * @param string $domainkey - * @param string $csr - * optional, same behavior as $reuseCsr from the original class, but we're passing the content of the csr already - * - * @throws \RuntimeException - * @return string[] - */ - public function signDomains(array $domains, $domainkey = null, $csr = null) - { - if (! $this->accountKey) { - throw new \RuntimeException("Account not initialized"); - } - - $this->log('Starting certificate generation process for domains'); - - $privateAccountKey = openssl_pkey_get_private($this->accountKey); - $accountKeyDetails = openssl_pkey_get_details($privateAccountKey); - - // start domains authentication - // ---------------------------- - - // Prepare order - $domains_in_order = array(); - foreach ($domains as $domain) { - $domains_in_order[] = array( - "type" => "dns", - "value" => $domain - ); - } - - // Send new-order request - $response = $this->signedRequest($this->req_uris['newOrder'], array( - "identifiers" => $domains_in_order - ), false); - - if ($this->client->getLastCode() == 403) { - $this->log("Got status 403 - setting LE status to unregistered."); - $this->acc_location = ''; - $this->setLeRegisteredState(0); - throw new \RuntimeException("Got 'unauthorized' response - we need to re-register at next run. Whole response: " . json_encode($response)); - } - - // if response is not an array but a string, it's most likely a server-error, e.g. - // ErrorAn error occurred while processing your request. - //

Reference #179.d8be1402.1458059103.3613c4db - if (! is_array($response)) { - throw new \RuntimeException("Invalid response from LE for domain $domain. Whole response: " . json_encode($response)); - } - - if (! array_key_exists('authorizations', $response)) { - throw new \RuntimeException("No authorizations received for $domain. Whole response: " . json_encode($response)); - } - - $authorizations = $response['authorizations']; - $finalizeLink = $response['finalize']; - - $i = 0; - - foreach ($authorizations as $authorization) { - - // 1. getting available authentication options - // ------------------------------------------- - - $domain = $response['identifiers'][$i ++]['value']; - - $this->log("Requesting challenge for $domain"); - - // get authorization - $auth_response = $this->client->get($authorization); - - if (! array_key_exists('challenges', $auth_response)) { - throw new \RuntimeException("No challenges received for $domain. Whole response: " . json_encode($auth_response)); - } - - // choose http-01 challenge only - $challenge = array_reduce($auth_response['challenges'], function ($v, $w) { - return $v ? $v : ($w['type'] == 'http-01' ? $w : false); - }); - - if (! $challenge) { - throw new \RuntimeException("HTTP Challenge for $domain is not available. Whole response: " . json_encode($response)); - } - - $this->log("Got challenge token for $domain"); - $location = $challenge['url']; - - // 2. saving authentication token for web verification - // --------------------------------------------------- - - $directory = Settings::Get('system.letsencryptchallengepath') . '/.well-known/acme-challenge'; - $tokenPath = $directory . '/' . $challenge['token']; - - if (! file_exists($directory) && ! @mkdir($directory, 0755, true)) { - throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); - } - - $header = array( - // need to be in precise order! - "e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]), - "kty" => "RSA", - "n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"]) - ); - $payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true)); - - file_put_contents($tokenPath, $payload); - chmod($tokenPath, 0644); - - // 3. verification process itself - // ------------------------------- - - $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - - $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); - - // simple self check - if (Settings::Get('system.disable_le_selfcheck') == '0') { - $selfcheckpayload = \Froxlor\Http\HttpClient::urlGet($uri, false); - if ($payload !== trim($selfcheckpayload)) { - $errmsg = json_encode(error_get_last()); - if ($errmsg != "null") { - $errmsg = "; PHP error: " . $errmsg; - } else { - $errmsg = ""; - } - $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "[Lets Encrypt self-check] Please check $uri - token seems to be not available. This is just a simple self-check, it might be wrong but consider using this information when Let's Encrypt fails to issue a certificate" . $errmsg); - } - } - - $this->log("Sending request to challenge"); - - // send request to challenge - $result = $this->signedRequest($challenge['url'], array( - "type" => "http-01", - "keyAuthorization" => $payload, - "token" => $challenge['token'] - ), false); - - // waiting loop - // we wait for a maximum of 30 seconds to avoid endless loops - $count = 0; - do { - if (empty($result['status']) || $result['status'] == "invalid") { - @unlink($tokenPath); - throw new \RuntimeException("Verification ended with error: " . json_encode($result)); - } - $ended = ! ($result['status'] === "pending" || $result['status'] === "processing"); - - if (! $ended) { - $this->log("Verification " . $result['status'] . ", sleeping 1s"); - sleep(1); - $count ++; - } - - $result = $this->client->get($location); - } while (! $ended && $count < 30); - - $this->log("Verification ended with status: ${result['status']}"); - @unlink($tokenPath); - } - - // requesting certificate - // ---------------------- - - // generate private key for domain if not exist - if (empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) { - $keys = $this->generateKey(); - $domainkey = $keys['private']; - } - - // load domain key - $privateDomainKey = openssl_pkey_get_private($domainkey); - - if (empty($csr)) { - $csr = $this->generateCSR($privateDomainKey, $domains); - } - - // request certificates creation - $result = $this->signedRequest($finalizeLink, array( - 'csr' => $csr - ), false); - if ($this->client->getLastCode() !== 200) { - throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result)); - } - if (! isset($result['certificate'])) { - throw new \RuntimeException("No certificate URL specified in result"); - } - - $certificates = array(); - $certdata = $this->client->get($result['certificate']); - $this->log("Got certificate! YAY!"); - $certificates[] = $certdata; - foreach ($this->client->getLastLinks() as $link) { - $this->log("Requesting chained cert at $link"); - $result = $this->client->get($link); - $certificates[] = $result; - } - - if (empty($certificates)) - throw new \RuntimeException('No certificates generated'); - - $fullchain = implode("\n", $certificates); - $crt = array_shift($certificates); - $chain = implode("\n", $certificates); - - $this->log("Done, returning new certificates and key"); - return array( - 'fullchain' => $fullchain, - 'crt' => $crt, - 'chain' => $chain, - 'key' => $domainkey, - 'csr' => $csr - ); - } - - private function setLeRegisteredState($state) - { - if ($this->isLeProduction) { - if ($this->isFroxlorVhost) { - Settings::Set('system.leregistered', $state); - Settings::Set('system.leaccount', $this->acc_location); - } else { - $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `leregistered` = :registered, `leaccount` = :kid WHERE `customerid` = :customerid;"); - Database::pexecute($upd_stmt, array( - 'registered' => $state, - 'kid' => $this->acc_location, - 'customerid' => $this->customerId - )); - } - } - } - - private function parsePemFromBody($body) - { - $pem = chunk_split(base64_encode($body), 64, "\n"); - return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; - } - - private function postNewReg() - { - $this->log('Getting last terms of service URL'); - $directory = $this->client->get('/directory'); - if (! isset($directory['meta']) || ! isset($directory['meta']['termsOfService'])) { - throw new \RuntimeException("No terms of service link available!"); - } - $this->log('Sending registration to letsencrypt server'); - - return $this->signedRequest($this->req_uris['newAccount'], array( - 'termsOfServiceAgreed' => true - )); - } - - private function generateCSR($privateKey, array $domains) - { - $domain = reset($domains); - $san = implode(",", array_map(function ($dns) { - return "DNS:" . $dns; - }, $domains)); - $tmpConf = tmpfile(); - $tmpConfMeta = stream_get_meta_data($tmpConf); - $tmpConfPath = $tmpConfMeta["uri"]; - - // workaround to get SAN working - fwrite($tmpConf, 'HOME = . -RANDFILE = $ENV::HOME/.rnd -[ req ] -default_bits = ' . Settings::Get('system.letsencryptkeysize') . ' -default_keyfile = privkey.pem -distinguished_name = req_distinguished_name -req_extensions = v3_req -[ req_distinguished_name ] -countryName = Country Name (2 letter code) -[ v3_req ] -basicConstraints = CA:FALSE -subjectAltName = ' . $san . ' -keyUsage = nonRepudiation, digitalSignature, keyEncipherment'); - - $csr = openssl_csr_new(array( - "CN" => $domain, - "ST" => Settings::Get('system.letsencryptstate'), - "C" => Settings::Get('system.letsencryptcountrycode'), - "O" => "Unknown" - ), $privateKey, array( - "config" => $tmpConfPath, - "digest_alg" => "sha256" - )); - - if (! $csr) - throw new \RuntimeException("CSR couldn't be generated! " . openssl_error_string()); - - openssl_csr_export($csr, $csr); - fclose($tmpConf); - - preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); - - return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); - } - - private function generateKey() - { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_RSA, - "private_key_bits" => (int) Settings::Get('system.letsencryptkeysize') - )); - - if (! openssl_pkey_export($res, $privateKey)) { - throw new \RuntimeException("Key export failed!"); - } - - $details = openssl_pkey_get_details($res); - - return array( - 'private' => $privateKey, - 'public' => $details['key'] - ); - } - - private function signedRequest($uri, array $payload, $needs_jwk = true) - { - $privateKey = openssl_pkey_get_private($this->accountKey); - $details = openssl_pkey_get_details($privateKey); - - $header = array( - "alg" => "RS256" - ); - - if ($needs_jwk) { - $header["jwk"] = array( - "kty" => "RSA", - "n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]), - "e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"]) - ); - } else { - // need account-url - $header["kid"] = $this->acc_location; - } - - $protected = $header; - $protected["nonce"] = $this->client->getLastNonce(); - $protected["url"] = $uri; - - $payload64 = Base64UrlSafeEncoder::encode(json_encode($payload, JSON_UNESCAPED_SLASHES)); - $protected64 = Base64UrlSafeEncoder::encode(json_encode($protected)); - - openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256"); - - $signed64 = Base64UrlSafeEncoder::encode($signed); - - $data = array( - 'protected' => $protected64, - 'payload' => $payload64, - 'signature' => $signed64 - ); - - $this->log("Sending signed request to $uri"); - return $this->client->post($uri, json_encode($data)); - } - - protected function log($message) - { - $this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "letsencrypt-v2 " . $message); - } -} diff --git a/lng/english.lng.php b/lng/english.lng.php index e9fe8534..ee1fed12 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1833,17 +1833,15 @@ $lng['error']['nowildcardwithletsencrypt'] = 'Let\'s Encrypt cannot handle wildc $lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; $lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates'; $lng['serversettings']['letsencryptca']['title'] = "Let's Encrypt environment"; -$lng['serversettings']['letsencryptca']['description'] = "Environment to be used for Let's Encrypt certificates.
ATTENTION: Let's Encrypt is still in beta"; +$lng['serversettings']['letsencryptca']['description'] = "Environment to be used for Let's Encrypt certificates."; $lng['serversettings']['letsencryptcountrycode']['title'] = "Let's Encrypt country code"; -$lng['serversettings']['letsencryptcountrycode']['description'] = "2 letter country code used to generate Let's Encrypt certificates.
ATTENTION: Let's Encrypt is still in beta"; +$lng['serversettings']['letsencryptcountrycode']['description'] = "2 letter country code used to generate Let's Encrypt certificates."; $lng['serversettings']['letsencryptstate']['title'] = "Let's Encrypt state"; -$lng['serversettings']['letsencryptstate']['description'] = "State used to generate Let's Encrypt certificates.
ATTENTION: Let's Encrypt is still in beta"; +$lng['serversettings']['letsencryptstate']['description'] = "State used to generate Let's Encrypt certificates."; $lng['serversettings']['letsencryptchallengepath']['title'] = "Path for Let's Encrypt challenges"; -$lng['serversettings']['letsencryptchallengepath']['description'] = "Directory where the Let's Encrypt challenges should be offered from via a global alias.
ATTENTION: Let's Encrypt is still in beta"; +$lng['serversettings']['letsencryptchallengepath']['description'] = "Directory where the Let's Encrypt challenges should be offered from via a global alias."; $lng['serversettings']['letsencryptkeysize']['title'] = "Key size for new Let's Encrypt certificates"; -$lng['serversettings']['letsencryptkeysize']['description'] = "Size of the key in Bits for new Let's Encrypt certificates.
ATTENTION: Let's Encrypt is still in beta"; -$lng['serversettings']['letsencryptreuseold']['title'] = "Re-use Let's Encrypt key"; -$lng['serversettings']['letsencryptreuseold']['description'] = "If activated, the same key will be used for every renew, otherwise a new key will be generated every time.
ATTENTION: Let's Encrypt is still in beta"; +$lng['serversettings']['letsencryptkeysize']['description'] = "Size of the key in Bits for new Let's Encrypt certificates."; $lng['serversettings']['leenabled']['title'] = "Enable Let's Encrypt"; $lng['serversettings']['leenabled']['description'] = "If activated, customers are able to let froxlor automatically generate and renew Let's Encrypt ssl-certificates for domains with a ssl IP/port.

Please remember that you need to go through the webserver-configuration when enabled because this feature needs a special configuration."; $lng['domains']['ssl_redirect_temporarilydisabled'] = "
The SSL redirect is temporarily deactivated while a new Let's Encrypt certificate is generated. It will be activated again after the certificate was generated."; @@ -2054,3 +2052,5 @@ $lng['tasks']['remove_pdns_domain'] = 'Delete domain %s from PowerDNS database'; $lng['admin']['novhostcontainer'] = '

None of the IPs and ports has the "' . $lng['admin']['ipsandports']['create_vhostcontainer'] . '" option enabled, many settings here will not be available'; $lng['serversettings']['errorlog_level']['title'] = 'Error log-level'; $lng['serversettings']['errorlog_level']['description'] = 'Specify the error log level. Default is "warn" for apache-users and "error" for nginx-users.'; +$lng['serversettings']['letsencryptecc']['title'] = "Issue ECC / ECDSA certificate"; +$lng['serversettings']['letsencryptecc']['description'] = "If set to a valid key-size the certificate issued will use ECC / ECDSA"; diff --git a/lng/german.lng.php b/lng/german.lng.php index 58600e33..800f7663 100644 --- a/lng/german.lng.php +++ b/lng/german.lng.php @@ -1485,17 +1485,15 @@ $lng['error']['nowildcardwithletsencrypt'] = 'Let\'s Encrypt kann in ACME v1 nic $lng['panel']['letsencrypt'] = 'Benutzt Let\'s encrypt'; $lng['crondesc']['cron_letsencrypt'] = 'Aktualisierung der Let\'s Encrypt Zertifikate'; $lng['serversettings']['letsencryptca']['title'] = "Let's Encrypt Umgebung"; -$lng['serversettings']['letsencryptca']['description'] = "Let's Encrypt - Umgebung, welche genutzt wird um Zertifikate zu bestellen.
ACHTUNG: Let's Encrypt befindet sich noch im Test"; +$lng['serversettings']['letsencryptca']['description'] = "Let's Encrypt - Umgebung, welche genutzt wird um Zertifikate zu bestellen."; $lng['serversettings']['letsencryptcountrycode']['title'] = "Let's Encrypt Ländercode"; -$lng['serversettings']['letsencryptcountrycode']['description'] = "2 - stelliger Ländercode, welcher benutzt wird um Let's Encrypt - Zertifikate zu bestellen.
ACHTUNG: Let's Encrypt befindet sich noch im Test"; +$lng['serversettings']['letsencryptcountrycode']['description'] = "2 - stelliger Ländercode, welcher benutzt wird um Let's Encrypt - Zertifikate zu bestellen."; $lng['serversettings']['letsencryptstate']['title'] = "Let's Encrypt Bundesland"; -$lng['serversettings']['letsencryptstate']['description'] = "Bundesland, welches benutzt wird, um Let's Encrypt - Zertifikate zu bestellen.
ACHTUNG: Let's Encrypt befindet sich noch im Test"; +$lng['serversettings']['letsencryptstate']['description'] = "Bundesland, welches benutzt wird, um Let's Encrypt - Zertifikate zu bestellen."; $lng['serversettings']['letsencryptchallengepath']['title'] = "Verzeichnis für Let's Encrypt challenges"; -$lng['serversettings']['letsencryptchallengepath']['description'] = "Let's Encrypt challenges werden aus diesem Verzeichnis über einen globalen Alias ausgeliefert.
ACHTUNG: Let's Encrypt befindet sich noch im Test"; +$lng['serversettings']['letsencryptchallengepath']['description'] = "Let's Encrypt challenges werden aus diesem Verzeichnis über einen globalen Alias ausgeliefert."; $lng['serversettings']['letsencryptkeysize']['title'] = "Schlüsselgröße für neue Let's Encrypt Zertifikate"; -$lng['serversettings']['letsencryptkeysize']['description'] = "Größe des Schlüssels in Bit für neue Let's Encrypt Zertifikate.
ACHTUNG: Let's Encrypt befindet sich noch im Test"; -$lng['serversettings']['letsencryptreuseold']['title'] = "Let's Encrypt Schlüssel wiederverwenden"; -$lng['serversettings']['letsencryptreuseold']['description'] = "Wenn dies aktiviert ist, wird der alte Schlüssel bei jeder Verlängerung verwendet, andernfalls wird ein neues Paar generiert.
ACHTUNG: Let's Encrypt befindet sich noch im Test"; +$lng['serversettings']['letsencryptkeysize']['description'] = "Größe des Schlüssels in Bit für neue Let's Encrypt Zertifikate."; $lng['serversettings']['leenabled']['title'] = "Let's Encrypt verwenden"; $lng['serversettings']['leenabled']['description'] = "Wenn dies aktiviert ist, können Kunden durch Froxlor automatisch generierte und verlängerbare Let's Encrypt SSL-Zertifikate für Domains mit SSL IP/Port nutzen.

Bitte die Webserver-Konfiguration beachten wenn aktiviert, da dieses Feature eine spezielle Konfiguration benötigt."; $lng['domains']['ssl_redirect_temporarilydisabled'] = "
Die SSL-Umleitung ist, während ein neues Let's Encrypt - Zertifikat erstellt wird, temporär deaktiviert. Die Umleitung wird nach der Zertifikatserstellung wieder aktiviert."; @@ -1701,3 +1699,5 @@ $lng['tasks']['remove_pdns_domain'] = 'Lösche Domain %s von PowerDNS Datenbank' $lng['admin']['novhostcontainer'] = '

Keine der IPs und Ports hat die Option "' . $lng['admin']['ipsandports']['create_vhostcontainer'] . '" aktiviert, einige Einstellungen sind daher nicht verfügbar.'; $lng['serversettings']['errorlog_level']['title'] = 'Ausführlichkeit des Fehlerprotokolls'; $lng['serversettings']['errorlog_level']['description'] = 'Steuert die Ausführlichkeit des Fehlerprotokolls. Voreinstellung ist "warn" bei Apache und "error" bei Nginx.'; +$lng['serversettings']['letsencryptecc']['title'] = "ECC / ECDSA Zertifikate ausstellen"; +$lng['serversettings']['letsencryptecc']['description'] = "Wenn eine Schlüsselgröße ausgewählt wird, werden ECC / ECDSA Zertifikate erstellt";