From 05129e812c0199bf6ec686c95173849c8f8d8a2e Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Wed, 30 Mar 2016 00:06:12 +0200 Subject: [PATCH 01/14] acme client package for pfSense, initial commit --- security/pfSense-pkg-acme/Makefile | 64 ++ .../files/etc/inc/priv/acme.priv.inc | 36 + .../pfSense-pkg-acme/files/pkg-deinstall.in | 3 + .../pfSense-pkg-acme/files/pkg-install.in | 7 + .../files/usr/local/pkg/acme.xml | 70 ++ .../files/usr/local/pkg/acme/acme.inc | 405 ++++++++++ .../files/usr/local/pkg/acme/acme_gui.inc | 146 ++++ .../usr/local/pkg/acme/acme_htmllist.inc | 758 ++++++++++++++++++ .../files/usr/local/pkg/acme/acme_utils.inc | 132 +++ .../files/usr/local/pkg/acme/lescript.inc | 537 +++++++++++++ .../usr/local/pkg/acme/pkg_acme_tabs.inc | 26 + .../usr/local/share/pfSense-pkg-acme/info.xml | 11 + .../usr/local/www/acme/acme_accountkeys.php | 365 +++++++++ .../local/www/acme/acme_accountkeys_edit.php | 495 ++++++++++++ .../usr/local/www/acme/acme_certificates.php | 378 +++++++++ .../local/www/acme/acme_certificates_edit.php | 427 ++++++++++ .../local/www/acme/acme_generalsettings.php | 117 +++ security/pfSense-pkg-acme/pkg-descr | 3 + security/pfSense-pkg-acme/pkg-plist | 15 + 19 files changed, 3995 insertions(+) create mode 100644 security/pfSense-pkg-acme/Makefile create mode 100644 security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc create mode 100644 security/pfSense-pkg-acme/files/pkg-deinstall.in create mode 100644 security/pfSense-pkg-acme/files/pkg-install.in create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/pkg_acme_tabs.inc create mode 100644 security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml create mode 100644 security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php create mode 100644 security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys_edit.php create mode 100644 security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php create mode 100644 security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php create mode 100644 security/pfSense-pkg-acme/files/usr/local/www/acme/acme_generalsettings.php create mode 100644 security/pfSense-pkg-acme/pkg-descr create mode 100644 security/pfSense-pkg-acme/pkg-plist diff --git a/security/pfSense-pkg-acme/Makefile b/security/pfSense-pkg-acme/Makefile new file mode 100644 index 000000000000..1d0496d52bb1 --- /dev/null +++ b/security/pfSense-pkg-acme/Makefile @@ -0,0 +1,64 @@ +# $FreeBSD$ + +PORTNAME= pfSense-pkg-acme +PORTVERSION= 0.1 +CATEGORIES= security +MASTER_SITES= # empty +DISTFILES= # empty +EXTRACT_ONLY= # empty + +MAINTAINER= PiBa-NL +COMMENT= pfSense package acme + +RUN_DEPENDS= + +CONFLICTS= + +NO_BUILD= yes +NO_MTREE= yes + +SUB_FILES= pkg-install pkg-deinstall +SUB_LIST= PORTNAME=${PORTNAME} + +do-extract: + ${MKDIR} ${WRKSRC} + +do-install: + ${MKDIR} ${STAGEDIR}${PREFIX}/pkg + ${MKDIR} ${STAGEDIR}${PREFIX}/pkg/acme + ${MKDIR} ${STAGEDIR}${PREFIX}/www + ${MKDIR} ${STAGEDIR}${PREFIX}/www/acme + ${MKDIR} ${STAGEDIR}/etc/inc/priv + ${MKDIR} ${STAGEDIR}${DATADIR} + ${INSTALL_DATA} -m 0644 ${FILESDIR}${PREFIX}/pkg/acme.xml \ + ${STAGEDIR}${PREFIX}/pkg + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_gui.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_htmllist.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_utils.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/lescript.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/pkg_acme_tabs.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_accountkeys.php \ + ${STAGEDIR}${PREFIX}/www/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_accountkeys_edit.php \ + ${STAGEDIR}${PREFIX}/www/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_certificates.php \ + ${STAGEDIR}${PREFIX}/www/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_certificates_edit.php \ + ${STAGEDIR}${PREFIX}/www/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_generalsettings.php \ + ${STAGEDIR}${PREFIX}/www/acme + ${INSTALL_DATA} ${FILESDIR}/etc/inc/priv/acme.priv.inc \ + ${STAGEDIR}/etc/inc/priv + ${INSTALL_DATA} ${FILESDIR}${DATADIR}/info.xml \ + ${STAGEDIR}${DATADIR} + @${REINPLACE_CMD} -i '' -e "s|%%PKGVERSION%%|${PKGVERSION}|" \ + ${STAGEDIR}${DATADIR}/info.xml + +.include diff --git a/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc b/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc new file mode 100644 index 000000000000..7a145b9023dd --- /dev/null +++ b/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc @@ -0,0 +1,36 @@ + + + + + + + + acme + Acme + /usr/local/pkg/acme/acme.inc + + Acme Certificates + +
Services
+ /acme/acme_certificates.php +
+ + Acme + Automated Certificate Management Environment + + + + plugin_certificates + + + installedpackages->acme->config + + acme_custom_php_install_command(); + + + acme_custom_php_deinstall_command(); + +
diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc new file mode 100644 index 000000000000..246a79a88591 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -0,0 +1,405 @@ + + +$a_enabledisable = array(); +$a_enabledisable['enable'] = array('name' => 'Enabled'); +$a_enabledisable['disable'] = array('name' => 'Disabled'); + +global $a_acmeserver; +$a_acmeserver = array(); +$a_acmeserver['letsencrypt-staging'] = array('name' => "Let's Encrypt Staging (for TESTING purposes)", + 'url' => 'https://acme-staging.api.letsencrypt.org' +); +$a_acmeserver['letsencrypt-production'] = array('name' => "Let's Encrypt Production(Applies ratelimits to certificate requests)", + 'url' => 'https://acme-v01.api.letsencrypt.org' +); + +global $acme_domain_validation_method; +$acme_domain_validation_method = array(); +$acme_domain_validation_method['webroot'] = array(name => "local webroot folder", + 'fields' => array( + 'folder' => array('name'=>"folder",'columnheader'=>"RootFolder",'type'=>"textbox",'size'=>"50", + 'description' =>"Folder the acme challenge response is written to for example: /usr/local/www/.well-known/acme-challenge/" + ) + )); + +/* +$acme_domain_validation_method['ftpwebroot'] = array(name => "ftpwebroot", + 'fields' => array( + 'server' => array('name'=>"ftpserver",'columnheader'=>"Server",'type'=>"textbox",'size'=>"50", + 'description' =>"Hostname of FTP server to connect to for example ftp://www.webserver.tld" + ), + 'method' => array('name'=>"method",'columnheader'=>"Method",'type'=>"textbox",'size'=>"50", + 'description' =>"Method (scp/sftp/ftp/other) to connect to the remote server" + ), + 'username' => array('name'=>"username",'columnheader'=>"Username",'type'=>"textbox",'size'=>"50", + 'description' =>"Username for the remote server" + ), + 'password' => array('name'=>"password",'columnheader'=>"Password",'type'=>"textbox",'size'=>"50", + 'description' =>"Password used to authenticate to the server" + ), + 'folder' => array('name'=>"folder",'columnheader'=>"Folder",'type'=>"textbox",'size'=>"50", + 'description' =>"Folder the acme challenge response is written to for default: /.well-known/acme-challenge/" + ) + )); +$acme_domain_validation_method['http-post'] = array(name => "http-post", + 'fields' => array( + 'url' => array('name'=>"url",'columnheader'=>"Url",'type'=>"textbox",'size'=>"50", + 'description' =>"Url the challenge file is posted to, the webserver there must store and reply to the request when the acme servers perform the request for the file from /.well-known/acme-challenge/" + ) + )); +*/ + +$acme_newcertificateactions = array(); +$acme_newcertificateactions['shellcommand'] = array(name => "shell command"); +//$acme_domain_validation_method['php command'] = array(name => "php command script"); + +// +#end + +function set_cronjob() { + global $config; + $a_global = &$config['installedpackages']['acme']; + if (isset($a_global['enable'])) { + install_cron_job("/etc/rc.acme_renew.sh", true, "16", "3"); + } else { + install_cron_job("/etc/rc.acme_renew.sh", false); + } +} + +function acme_custom_php_deinstall_command() { + global $static_output; + $static_output .= "Acme, running acme_custom_php_deinstall_command()\n"; + update_output_window($static_output); + $static_output .= "Acme, deleting renew_renew.sh\n"; + update_output_window($static_output); + unlink_if_exists("/etc/rc.acme_renew.sh"); + $static_output .= "Acme, uninstalling cron job\n"; + update_output_window($static_output); + install_cron_job("/etc/rc.acme_renew.sh", false); + $static_output .= "Acme, running acme_custom_php_deinstall_command() DONE\n"; + update_output_window($static_output); +} + +function acme_custom_php_install_command() { + global $g, $config, $static_output; + $static_output .= "Acme, running acme_custom_php_install_command()\n"; + update_output_window($static_output); + + $acme_renew = << + +EOD; + // removing the \r prevents the "No input file specified." error.. + $acme_renew = str_replace("\r\n","\n", $acme_renew); + $fd = fopen("/etc/rc.acme_renew.sh", "w"); + fwrite($fd, $acme_renew); + fclose($fd); + chmod("/etc/rc.acme_renew.sh", 0755); + + set_cronjob(); + + $static_output .= "Acme, running acme_custom_php_install_command() DONE\n"; + update_output_window($static_output); +} + +function get_itembyname($a_array, $name) { + $i = 0; + if (is_array($a_array)) { + foreach ($a_array as $key => $item) { + if ($item['name'] == $name) { + return $i; + } + $i++; + } + } + return null; +} + +function get_accountkey_id($name) { + global $config; + $a_array = &$config['installedpackages']['acme']['accountkeys']['item']; + return get_itembyname($a_array, $name); +} +function get_accountkey($name) { + global $config; + $a_array = &$config['installedpackages']['acme']['accountkeys']['item']; + $id = get_accountkey_id($name); + return $a_array[$id]; +} + +function get_certificate_id($name) { + global $config; + $a_certificates = &$config['installedpackages']['acme']['certificates']['item']; + $i = 0; + if (is_array($a_certificates)) { + foreach ($a_certificates as $key => $certificate) { + if ($certificate['name'] == $name) { + return $i; + } + $i++; + } + } + return null; +} + +function & get_certificate($name) { + global $config; + $a_certificates = &$config['installedpackages']['acme']['certificates']['item']; + $id = get_certificate_id($name); + if (is_numeric($id)) { + return $a_certificates[$id]; + } + return null; +} + + function createAcmeAccountKey(){ + $certificatename = "acme_account_key"; + $cert = lookup_cert_by_name($certificatename); + if (!is_array($cert)) { + global $config; + $a_cert =& $config['cert']; + $cert = array(); + $cert['refid'] = uniqid(); + $cert['descr'] = $certificatename; + $accountkey = generateAccountKey(); + cert_import($cert, $accountkey['crt'], $accountkey['prv']); + $a_cert[] = $cert; + $changedesc = "Services: Acme"; + $changedesc .= " created acme account key"; + write_config($changedesc); + } + } + function generateAccountKey() + { + $res = openssl_pkey_new(array( + "private_key_type" => OPENSSL_KEYTYPE_RSA, + "private_key_bits" => 4096, + )); + if(!openssl_pkey_export($res, $privateKey)) { + throw new \RuntimeException("Key export failed!"); + } + return $privateKey; + } + + function getAcmeClient($ca) { + $logger = new Logger(); + $le = \Analogic\ACME\Lescript::createWithCustomEvents($ca, $logger); + return $le; + } + function renew_all_certificates(){ + global $config; + $a_global = &$config['installedpackages']['acme']; + + foreach($a_global['certificates']['item'] as $certificate) { + echo "Checking if renewal is needed for: {$certificate['name']}\n"; + renew_certificate($certificate['name']); + } + } + + function renew_certificate($id, $force = false) { + $certificate = & get_certificate($id); + if (!$force) { + if ($certificate['status'] != 'active') { + echo "Certificate renewal for this certificate is set to: disabled\n"; + return; + } + + $timetorenew = false; + $now = new \DateTime(); + $lastrenewal = new \DateTime(); + $lastrenewal->setTimestamp($certificate['lastrenewal']); + $nextrenewal = $lastrenewal->add(new \DateInterval('P'.$certificate['renewafter'].'D')); + if ($now >= $nextrenewal) { + echo "## Its time to renew ##\n"; + $timetorenew = true; + } + } + + if ($timetorenew || $force) { + syslog(LOG_NOTICE, "Acme, renewing certificate: {$id}"); + echo "Renewing certificate"; + $domainstosign = array(); + foreach($certificate['a_domainlist']['item'] as $domain) { + $domainstosign[] = $domain['name']; + } + + echo "account: {$certificate['acmeaccount']} \n"; + $account = get_accountkey($certificate['acmeaccount']); + $acmeserver = $account['acmeserver']; + $key = $account['accountkey']; + echo "server: $acmeserver \n"; + global $a_acmeserver; + $url = $a_acmeserver[$acmeserver]['url']; + $le = getAcmeClient($url); + $le->setPrivateKey(base64_decode($key)); + + $handler = new acme_handler(); + $handler->certificateinfo = & $certificate; + $handler->path = ""; + $le->callback = $handler; + $le->signDomains($domainstosign); + + foreach($certificate['a_actionlist']['item'] as $action) { + if ($action['method'] == "shellcommand") { + echo "Running {$action['command']}\n"; + mwexec_bg($action['command']); + } + } + syslog(LOG_NOTICE, "Acme, certificate renewed: {$id}"); + + } + } + class acme_handler { + public $path = ""; + public $certificateinfo = null; + function chalenge_response_put($domain, $token, $payload){ + foreach($this->certificateinfo['a_domainlist']['item'] as $domainitem) { + if($domainitem['name'] == $domain){ + $domain_info = $domainitem; + } + } + //echo "





domain_info({$domain})
"; + //print_r($domain_info); + if ($domain_info['method'] == 'webroot') { + //echo "
USING WEBROOT"; + $directory = $domain_info['webrootfolder']; + if(!file_exists($directory) && !@mkdir($directory, 0755, true)) { + throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); + } + $tokenPath = $directory . "/" . $token; + //echo "
Saving token in:".$tokenPath; + file_put_contents($tokenPath, $payload); + } + } + function chalenge_response_cleanup($domain, $token){ + foreach($this->certificateinfo['a_domainlist']['item'] as $domainitem) { + if($domainitem['name'] == $domain){ + $domain_info = $domainitem; + } + } + if ($domain_info['method'] == 'webroot') { + @unlink($tokenPath); + } + } + function getCertificatePSK(){ + $certificatename = "acme_cert_" . $this->certificateinfo['name']; + $cert = lookup_cert_by_name($certificatename); + if (!is_array($cert)) { + global $config; + $a_cert =& $config['cert']; + $cert = array(); + $cert['refid'] = uniqid(); + $cert['descr'] = $certificatename; + $accountkey = generateAccountKey(); + //cert_import($cert, $accountkey['crt'], $accountkey['prv']); + //$cert['crt'] = base64_encode($crt_str); + $cert['prv'] = base64_encode($accountkey['prv']); + $a_cert[] = $cert; + } + return base64_decode($cert['prv']); + } + function storeCertificate($certificates){ + $certificatename = "acme_cert_" . $this->certificateinfo['name']; + global $config; + if (is_array($config['cert'])) { + foreach ($config['cert'] as &$cert) { + if ($cert['descr'] == $certificatename) { + //TODO add validation that the new cert 'fits' on the private key.. + $cert['crt'] = base64_encode(array_shift($certificates)); + + $id = get_certificate_id($this->certificateinfo['name']); + $a_certificates = &$config['installedpackages']['acme']['certificates']['item']; + $a_certificates[$id]['lastrenewal'] = time(); + } + } + } + // store chain to.?. + //$cert = lookup_cert_by_name($certificatename); + + //if (is_array($cert)) { + //$cert['crt'] = array_shift($certificates); + //echo "NEW CRT: ".base64_decode($cert['crt']); + //} + $changedesc = "Services: Acme"; + $changedesc .= "Storing signed certificate"; + write_config($changedesc); + } + } + + function registerAcmeAccountKey($ca, $key) { + //public $ca = 'https://acme-v01.api.letsencrypt.org'; + //$ca = 'https://acme-staging.api.letsencrypt.org'; // testing + + //$certificatename = "acme_account_key"; + //$cert = lookup_cert_by_name($certificatename); + //$key = $cert['prv']; + + //echo "




ACME result:
"; + $logger = new Logger(); + $le = \Analogic\ACME\Lescript::createWithCustomEvents($ca, $logger); + //$le->setPrivateKey(base64_decode($key)); + $le->setPrivateKey($key); + $result = $le->postNewReg(); + //print_r($result); + //echo ""; + } + class Logger { + function __call($name, $arguments) { + echo date('Y-m-d H:i:s')." [$name] ${arguments[0]}\n"; + } + } +?> diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc new file mode 100644 index 000000000000..aa3c2e4dec63 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc @@ -0,0 +1,146 @@ + array( + "faicon" => "fa-arrow-up", + "icon" => "icon_up.gif", + "iconsize" => 17), + 'movedown' => array( + "faicon" => "fa-arrow-down", + "icon" => "icon_down.gif", + "iconsize" => 17), + 'add' => array( + "faicon" => "fa-level-down", + "icon" => "icon_plus.gif", + "iconsize" => 17), + 'delete' => array( + "faicon" => "fa-trash", + "icon" => "icon_x.gif", + "iconsize" => 17), + 'edit' => array( + "faicon" => "fa-pencil", + "icon" => "icon_e.gif", + "iconsize" => 17), + 'clone' => array( + "faicon" => "fa-clone", + "icon" => "icon_plus.gif"), + 'acl' => array( + "faicon" => "fa-random", + "icon" => "icon_ts_rule.gif", + "iconsize" => 11), + 'cert' => array( + "faicon" => "fa-lock", + "icon" => "icon_frmfld_cert.png", + "iconsize" => 11), + 'advanced' => array( + "faicon" => "fa-cog", + "icon" => "icon_advanced.gif", + "iconsize" => 11), + 'enabled' => array( + "faicon" => "fa-check", + "icon" => "icon_pass.gif", + "iconsize" => 11), + 'disabled' => array( + "faicon" => "fa-ban", + "icon" => "icon_reject.gif", + "iconsize" => 11), + 'stats' => array( + "faicon" => "fa-tasks", + "icon" => "icon_log_s.gif", + "iconsize" => 11), + 'stop' => array( + "faicon" => "fa-stop-circle-o", + "icon" => "icon_service_stop.gif", + "iconsize" => 17), + 'start' => array( + "faicon" => "fa-play-circle", + "icon" => "icon_service_start.gif", + "iconsize" => 17), + 'up' => array( + "faicon" => "fa-check-circle", + "icon" => "icon_interface_up.gif", + "iconsize" => 11), + 'down' => array( + "faicon" => "fa-times-circle", + "icon" => "icon_interface_down.gif", + "iconsize" => 11), + 'resolvedns' => array( + "faicon" => "fa-info", + "icon" => "icon_log.gif", + "iconsize" => 11), + 'help' => array( + "faicon" => "fa-info-circle", + "icon" => "icon_help.gif", + "iconsize" => 11), + 'expand' => array( + "faicon" => "fa-plus-square-o", + "icon" => "plus.gif", + "iconsize" => 11) + +); + +global $pf_version; +$pf_version = substr(trim(file_get_contents("/etc/version")), 0, 3); + +function pf_version() { + global $pf_version; + return $pf_version; +} + +function acmeicon($iconname, $title) { + global $g, $acme_icons; + $title = htmlspecialchars($title); + $title = str_replace("'", "'", $title); + + $faicon = $acme_icons[$iconname]["faicon"]; + if (pf_version() < "2.3") { + $icon = $acme_icons[$iconname]["icon"]; + $iconsize = $acme_icons[$iconname]["iconsize"]; + return ""; + } else { + return ""; + } +} + +class Form_Section_class extends Form_Section { + // Shortcut, adds a group for the specified input + public function addInput(Form_Input $input, $groupclass = null) + { + $group = new Form_Group($input->getTitle()); + $group->add($input); + if ($groupclass) { + $group->addClass($groupclass); + } + $this->add($group); + return $input; + } +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc new file mode 100644 index 000000000000..376a8e349203 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc @@ -0,0 +1,758 @@ +_row_added(tableId, rowNr) + _field_changed(tableId, rowNr, fieldId, field) + */ + + public $tablename = ""; + public $fields = array(); + public $editmode = false; + public $fields_details = null; + public $keyfield = ""; + + public function __construct($tablename, $fields) { + $this->tablename = $tablename; + $this->fields = $fields; + } + + public function Draw($data) { + return $this->acme_htmllist($data, $this->fields, $this->editmode, $this->fields_details); + } + + public function outputjavascript() { + $table_def = array(); + $table_def['keyfield'] = $this->keyfield; + phparray_to_javascriptarray($table_def, "tabledefinition_".$this->tablename,Array('/*','/*/*')); + phparray_to_javascriptarray($this->fields, "fields_".$this->tablename,Array('/*','/*/name','/*/type','/*/text','/*/size','/*/items','/*/items/*','/*/items/*/*','/*/items/*/*/name')); + if (count($this->fields_details) != 0) { + phparray_to_javascriptarray($this->fields_details,"fields_details_".$this->tablename,Array('/*','/*/name','/*/columnheader','/*/description','/*/type','/*/text','/*/size','/*/items','/*/items/*','/*/items/*/*','/*/items/*/*/name','/*/items/*/*/name')); + } + } + + // function retrieves all posted values and returns an array + public function acme_htmllist_get_values() { + $values = array(); + + $rowindexes = $_POST[$this->tablename."_rowindex"]; + if (is_array($rowindexes)) { + foreach($rowindexes as $rowindex) { + $x = $rowindex; + $value = array(); + $add_item = false; + if (is_array($this->fields_details)) { + $fields = array_merge($this->fields, $this->fields_details); + } else { + $fields = $this->fields; + } + foreach($fields as $item) { + $itemname = $item['name']; + $value[$itemname] = $_POST[$this->tablename.$itemname.$x]; + if ($item['type'] == 'textarea') { + $value[$itemname] = base64_encode($value[$itemname]); + } + $add_item |= isset($_POST[$this->tablename.$itemname.$x]); + } + if ($add_item) { + if ($this->keyfield != "") { + if (isset($_POST[$this->tablename."_key".$x])) { + $key = $_POST[$this->tablename."_key".$x]; + } else { + $key = $_POST[$this->tablename.$this->keyfield.$x]; + } + } else { + $key = ""; + } + $index = $_POST[$this->tablename."_rowindex".$x]; + $value['_index'] = $index; + if (isset($values[$key])) { + $values[] = $value; + } else { + $values[$key] = $value; + } + } + } + } + return $values; + } + + function acme_htmllist_drawcell($item, $itemvalue, $editable, $itemname, $counter) { + $result = ""; + $itemnamenr = $this->tablename . $itemname . $counter; + $itemtype = $item['type']; + if ($editable) { + $itemtype = $item['type']; + if ($itemtype == "select") { + $result .= echo_html_select($itemnamenr, $item['items'], $itemvalue,"-none available-","html_listitem_change(\"{$this->tablename}\",\"{$itemname}\",\"{$counter}\",this);", ""); + } elseif ($itemtype == "checkbox") { + $checked = $itemvalue=='yes' ? " checked" : ""; + $result .= ""; + } elseif ($itemtype == "textarea") { + $result .= ""; + } elseif ($itemtype == "fixedtext") { + $result .= $item['text']; + } else { + $result .= ""; + } + } else { + if ($itemtype == "select") { + $result .= $item['items'][$itemvalue]['name']; + } elseif ($itemtype == "checkbox") { + $result .= $itemvalue=='yes' ? gettext('yes') : gettext('no'); + } elseif ($itemtype == "textarea") { + $result .= "
"; + $result .= str_replace(" "," ", str_replace("\n","
", htmlspecialchars(base64_decode($itemvalue)))); + $result .= '
'; + } elseif ($itemtype == "fixedtext") { + $result .= $item['text']; + } else { + $result .= htmlspecialchars($itemvalue); + } + } + return $result; + } + + function acme_htmllist($rowvalues, $items, $editstate=false, $itemdetails=null) { + + //echo "TEST
TEST
TEST
TEST
"; + //print_r($items); + //print_r($itemdetails); + + $tablename = $this->tablename; + global $g, $counter; + $result = ""; + $result .= " +
+
+ Table +
+
+ + + + "; + foreach($items as $item){ + $result .= ""; + } + $result .= " + + + "; + if (is_array($rowvalues)) { + foreach($rowvalues as $keyid => $value) { + if (!empty($this->keyfield)) { + if (preg_match("/[^0-9]/", $keyid)) { + $itemvalue = $keyid; + } else { + $itemvalue = $value[$this->keyfield]; + } + $key = ""; + } else { + $key = ""; + } + + if (!$editstate) { + $result .= " + + "; + + $leftitem = true; + foreach($items as $item) { + $result .= ""; + $leftitem = false; + + } + $result .= " + "; + $result .= ""; + } + $displaystyle = $editstate ? "" : "hidden"; + $result .= " + "; + foreach($items as $item){ + $itemname = $item['name']; + $itemvalue = $value[$itemname]; + $result .= ""; + $key = ""; + } + $result .= " + "; + $result .= ""; + if (isset($itemdetails)) { + $colspan = count($items); + $result .= ""; + $result .= << + '; + $result .= ""; + $result .= ""; + $result .= ""; + } + $counter++; + } + } + $result .= " +
{$item['columnheader']}Actions
+ + + "; + $itemname = $item['name']; + $itemvalue = $value[$itemname]; + if (isset($item['customdrawcell'])) { + $result .= $item['customdrawcell']($this, $item, $itemvalue, false, $itemname, $counter); + } else { + $result .= $this->acme_htmllist_drawcell($item, $itemvalue, false, $itemname, $counter); + } + $result .= " + ".acmeicon('edit','edit entry')." + ".acmeicon('delete','delete entry')." + ".acmeicon('clone','duplicate entry')." + "; + if (empty($this->noindex)) { + $result .= " + ".acmeicon('moveup','move row up')." + ".acmeicon('movedown','move row down')." + "; + } + $result .= " +
+ + + ".$key; + if (isset($item['customdrawcell'])) { + $result .= $item['customdrawcell']($this, $item, $itemvalue, true, $itemname, $counter); + } else { + $result .= $this->acme_htmllist_drawcell($item, $itemvalue, true, $itemname, $counter); + } + $result .= " + + ".acmeicon('delete','delete entry')." + ".acmeicon('clone','duplicate entry')." + "; + if (empty($this->noindex)) { + $result .= " + ".acmeicon('moveup','move row up')." + ".acmeicon('movedown','move row down')." + "; + } + $result .= " +
"; + $itemnr = 0; + $result .= "
"; + $leftitem = true; + foreach($itemdetails as $item) { + $itemname = $item['name']; + $itemvalue = $value[$itemname]; + if ($this->fields_details_showfieldfunction != null) { + // filter context un-related items through customizable function. + $fn = &$this->fields_details_showfieldfunction; + if ($fn($this, $itemname, $value) == false) { + continue; + } + } else + if (empty($itemvalue)) { + continue; + } + $result .= "
"; + if (!$leftitem) { + $result .= ", "; + } + $leftitem = false; + $result .= $item['columnheader'] . ": "; + if (isset($item['customdrawcell'])) { + $result .= $item['customdrawcell']($this, $item, $itemvalue, false, $itemname, $counter); + } else { + $result .= $this->acme_htmllist_drawcell($item, $itemvalue, false, $itemname, $counter); + } + $itemnr++; + $result .= "
"; + } + $result .= "
"; + $result .= ""; + $result .= "
"; + $result .= "
+ + ".acmeicon('add','add another entry')." + +
"; + return $result; + } +} + +function sort_index(&$a, &$b) { + // sort callback function, cannot be inside the object. + if ($a['_index'] != $b['_index']) { + return $a['_index'] > $b['_index'] ? 1 : -1; + } + return 0; +} + +function acme_htmllist_js(){ + global $g; +?> + + diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc new file mode 100644 index 000000000000..ebf3f7c8198a --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc @@ -0,0 +1,132 @@ + $item) + { + if (in_array($path.'/'.$key, $includeitems)) + $subpath = $path.'/'.$key; + else + $subpath = $path.'/*'; + if (in_array($subpath, $includeitems) || in_array($path.'/*', $includeitems)) { + if (is_array($item)) { + $subNodeName = "item$nestID"; + phparray_to_javascriptarray_recursive($nestID+1, $subpath, $items[$key], $subNodeName, $includeitems); + echo "{$offset}{$nodeName}['{$key}'] = $itemName;\n"; + } else { + $item = json_encode($item); + echo "{$offset}{$nodeName}['$key'] = $item;\n"; + } + } + } +} +function phparray_to_javascriptarray($items, $javaMapName, $includeitems) { + phparray_to_javascriptarray_recursive(1,'',$items, $javaMapName, $includeitems); +} + +function html_select_options($keyvaluelist, $selected="") { + $result = ""; + foreach($keyvaluelist as $key => $desc){ + $selectedhtml = $key == $selected ? "selected" : ""; + if ($desc['deprecated'] && $key != $selected){ + continue; + } + $name = htmlspecialchars($desc['name']); + $result .= ""; + } + return $result; +} + +function echo_html_select($name, $keyvaluelist, $selected, $listEmptyMessage="", $onchangeEvent="", $style="") { + $result = ""; + if (count($keyvaluelist)>0){ + if ($onchangeEvent != "") + $onchangeEvent = " onchange='$onchangeEvent'"; + if ($style != "") + $style = " style='$style'"; + $result .= ""; + } else { + $result .= $listEmptyMessage; + } + return $result; +} + +function form_keyvalue_array($hap_array) { + $result = array(); + foreach($hap_array as $key => $item) { + $result[$key] = $item['name']; + } + return $result; +} + +function form_name_array($hap_array) { + $result = array(); + foreach($hap_array as $key => $item) { + $name = $item['name']; + $result[$name] = $name; + } + return $result; +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc new file mode 100644 index 000000000000..149fef9999c9 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc @@ -0,0 +1,537 @@ + +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. + +source: https://github.com/analogic/lescript +*/ + +namespace Analogic\ACME; + +class Lescript +{ + //public $ca = 'https://acme-v01.api.letsencrypt.org'; + //public $ca = 'https://acme-staging.api.letsencrypt.org'; // testing + public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf'; + public $countryCode = 'CZ'; + public $state = "Czech Republic"; + + private $certificatesDir; + private $webRootDir; + + /** @var \Psr\Log\LoggerInterface */ + private $logger; + private $client; + //private $accountKeyPath; + private $privateKey = ""; + public $callback; + + protected function __construct() + { + } + + public static function createWithCustomEvents($ca, $logger = null) { + $instance = new static(); + $instance->logger = $logger; + //echo "CA: $ca"; + $instance->client = new Client($ca); + return $instance; + } + + public function createStandalone($certificatesDir, $webRootDir, $logger = null) + { + $this->certificatesDir = $certificatesDir; + $this->webRootDir = $webRootDir; + $this->logger = $logger; + $this->client = new Client($this->ca); + $this->accountKeyPath = $certificatesDir.'/_account/private.pem'; + } + + /*public function initAccount() + { + if(!is_file($this->accountKeyPath)) { + + // generate and save new private key for account + // --------------------------------------------- + + $this->log('Starting new account registration'); + $this->generateKey(dirname($this->accountKeyPath)); + $this->postNewReg(); + $this->log('New account certificate registered'); + + } else { + + $this->log('Account already registered. Continuing.'); + + } + }*/ + + public function signDomains(array $domains) + { + $this->log('Starting certificate generation process for domains'); + + $privateAccountKey = $this->readPrivateKey($this->accountKeyPath); + $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)) + ); + + // choose http-01 challange 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 = $this->webRootDir.'/.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);*/ + $this->callback->chalenge_response_put($domain, $challenge['token'], $payload); + + // 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($payload !== trim(@file_get_contents($uri))) { + throw new \RuntimeException("Please check $uri - token not available"); + } + + $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 + do { + if(empty($result['status']) || $result['status'] == "invalid") { + throw new \RuntimeException("Verification ended with error: ".json_encode($result)); + } + $ended = !($result['status'] === "pending"); + + if(!$ended) { + $this->log("Verification pending, sleeping 1s"); + sleep(1); + } + + $result = $this->client->get($location); + + } while (!$ended); + + $this->log("Verification ended with status: ${result['status']}"); + //@unlink($tokenPath); + $this->callback->chalenge_response_cleanup($domain, $challenge['token']); + } + + // requesting certificate + // ---------------------- + /*$domainPath = $this->getDomainPath(reset($domains)); + + // generate private key for domain if not exist + if(!is_dir($domainPath) || !is_file($domainPath.'/private.pem')) { + $this->generateKey($domainPath); + } + + // load domain key + $privateDomainKey = $this->readPrivateKey($domainPath.'/private.pem');*/ + $privateDomainKey = $this->readCertificatePSK(); + + $this->client->getLastLinks(); + + // request certificates creation + $result = $this->signedRequest( + "/acme/new-cert", + array('resource' => 'new-cert', 'csr' => $this->generateCSR($privateDomainKey, $domains)) + ); + 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); + + } else if ($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'); + + $this->log("Calling: storeCertificate"); + $this->callback->storeCertificate($certificates); + /*$this->log("Saving fullchain.pem"); + file_put_contents($domainPath.'/fullchain.pem', implode("\n", $certificates)); + + $this->log("Saving cert.pem"); + file_put_contents($domainPath.'/cert.pem', array_shift($certificates)); + + $this->log("Saving chain.pem"); + file_put_contents($domainPath."/chain.pem", implode("\n", $certificates)); + */ + + $this->log("Done !!§§!"); + } + + private function readCertificatePSK(){ + $psk = $this->callback->getCertificatePSK(); + if(($key = openssl_pkey_get_private($psk)) === FALSE) { + throw new \RuntimeException(openssl_error_string()); + } + return $key; + } + + private function readPrivateKey($path) + { + if (!empty($this->privateKey)) { + //echo " USING PSK from STRING ".$this->privateKey; + if(($key = openssl_pkey_get_private($this->privateKey)) === FALSE) { + throw new \RuntimeException(openssl_error_string()); + } + } else { + if(($key = openssl_pkey_get_private('file://'.$path)) === FALSE) { + throw new \RuntimeException(openssl_error_string()); + } + } + return $key; + } + + private function parsePemFromBody($body) + { + $pem = chunk_split(base64_encode($body), 64, "\n"); + return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; + } + + private function getDomainPath($domain) + { + return $this->certificatesDir.'/'.$domain.'/'; + } + + public function postNewReg() + { + $this->log('Sending registration to letsencrypt server'); + + return $this->signedRequest( + '/acme/new-reg', + array('resource' => 'new-reg', 'agreement' => $this->license) + ); + } + + 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 = 2048 +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" => $this->state, + "C" => $this->countryCode, + "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); + + file_put_contents($this->getDomainPath($domain)."/last.csr", $csr); + preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); + + return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); + } + + private function generateKey($outputDirectory) + { + $res = openssl_pkey_new(array( + "private_key_type" => OPENSSL_KEYTYPE_RSA, + "private_key_bits" => 4096, + )); + + if(!openssl_pkey_export($res, $privateKey)) { + throw new \RuntimeException("Key export failed!"); + } + + $details = openssl_pkey_get_details($res); + + if(!is_dir($outputDirectory)) @mkdir($outputDirectory, 0700, true); + if(!is_dir($outputDirectory)) throw new \RuntimeException("Cant't create directory $outputDirectory"); + + file_put_contents($outputDirectory.'/private.pem', $privateKey); + file_put_contents($outputDirectory.'/public.pem', $details['key']); + } + + public function setPrivateKey($psk) { + $this->privateKey = $psk; + + } + private function signedRequest($uri, array $payload) + { + $privateKey = $this->readPrivateKey($this->accountKeyPath); + $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) + { + if($this->logger) { + $this->logger->info($message); + } else { + echo $message."\n"; + } + } +} + +class Client +{ + private $lastCode; + private $lastHeader; + + private $base; + + public function __construct($base) + { + $this->base = $base; + } + + private function curl($method, $url, $data = null) + { + //echo "$method, $url, {$this->base}"; + $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]; + } +} + +class Base64UrlSafeEncoder +{ + public static function encode($input) + { + return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + } + + public static function decode($input) + { + $remainder = strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= str_repeat('=', $padlen); + } + return base64_decode(strtr($input, '-_', '+/')); + } +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/pkg_acme_tabs.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/pkg_acme_tabs.inc new file mode 100644 index 000000000000..a9d63f13c34c --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/pkg_acme_tabs.inc @@ -0,0 +1,26 @@ + "General settings", url => "acme_generalsettings.php"); +$acme_tab_array['acme']['certificates'] = Array(name => "Certificates", url => "acme_certificates.php"); +$acme_tab_array['acme']['accountkeys'] = Array(name => "Account keys", url => "acme_accountkeys.php"); + +function display_top_tabs_active($top_tabs, $activetab) { + $tab_array = array(); + foreach($top_tabs as $key => $tab_item){ + $tab_array[] = array($tab_item['name'], $key == $activetab, $tab_item['url']); + } + display_top_tabs($tab_array); +} + +?> diff --git a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml new file mode 100644 index 000000000000..90398477ded1 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml @@ -0,0 +1,11 @@ + + + + Acme + https://doc.pfsense.org/index.php/pfsense-pkg-acme + + https://letsencrypt.org/ + %%PKGVERSION%% + acme.xml + + diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php new file mode 100644 index 000000000000..bb842d96d324 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php @@ -0,0 +1,365 @@ + $before and not selected */ + for ($i = $before+1; $i < count($items); $i++) { + if (!in_array($i, $selected)) { + $a_new[] = $items[$i]; + } + } + if (count($a_new) > 0) { + $items = $a_new; + } +} + +if($_POST['action'] == "toggle") { + $id = $_POST['id']; + echo "$id|"; + if (isset($a_certifcates[get_certificate_id($id)])) { + $frontent = &$a_certifcates[get_certificate_id($id)]; + if ($frontent['status'] != "disabled"){ + $frontent['status'] = 'disabled'; + echo "0|"; + }else{ + $frontent['status'] = 'active'; + echo "1|"; + } + $changedesc .= " set frontend '$id' status to: {$frontent['status']}"; + + touch($d_acmeconfdirty_path); + write_config($changedesc); + } + echo "ok|"; + exit; +} +if($_POST['action'] == "renew") { + $id = $_POST['id']; + echo $id . "\n"; + if (isset($a_certifcates[get_certificate_id($id)])) { + renew_certificate($id, true); + } + exit; +} + +if ($_POST) { + $pconfig = $_POST; + + if ($_POST['apply']) { + $result = haproxy_check_and_run($savemsg, true); + if ($result) { + unlink_if_exists($d_acmeconfdirty_path); + } + } elseif ($_POST['del_x']) { + /* delete selected rules */ + $deleted = false; + if (is_array($_POST['rule']) && count($_POST['rule'])) { + $selected = array(); + foreach($_POST['rule'] as $selection) { + $selected[] = get_certificate_id($selection); + } + foreach ($selected as $itemnr) { + unset($a_certifcates[$itemnr]); + $deleted = true; + } + if ($deleted) { + if (write_config("Acme, deleting certificate(s)")) { + //mark_subsystem_dirty('filter'); + touch($d_acmeconfdirty_path); + } + } + header("Location: acme_certificates.php"); + exit; + } + } else { + + // from '\src\usr\local\www\vpn_ipsec.php' + /* yuck - IE won't send value attributes for image buttons, while Mozilla does - so we use .x/.y to find move button clicks instead... */ + // TODO: this. is. nasty. + unset($delbtn, $delbtnp2, $movebtn, $movebtnp2, $togglebtn, $togglebtnp2); + foreach ($_POST as $pn => $pd) { + if (preg_match("/move_(.+)/", $pn, $matches)) { + $movebtn = $matches[1]; + } + } + // + + /* move selected p1 entries before this */ + if (isset($movebtn) && is_array($_POST['rule']) && count($_POST['rule'])) { + $moveto = get_frontend_id($movebtn); + $selected = array(); + foreach($_POST['rule'] as $selection) { + $selected[] = get_frontend_id($selection); + } + array_moveitemsbefore($a_certifcates, $moveto, $selected); + + touch($d_acmeconfdirty_path); + write_config($changedesc); + } + } +} else { + $result = null;//haproxy_check_config($retval); + if ($result) { + $savemsg = gettext($result); + } +} + +if ($_GET['act'] == "del") { + $id = $_GET['id']; + $id = get_certificate_id($id); + if (isset($a_certifcates[$id])) { + if (!$input_errors) { + unset($a_certifcates[$id]); + $changedesc .= " Frontend delete"; + write_config($changedesc); + touch($d_acmeconfdirty_path); + } + header("Location: acme_certificates.php"); + exit; + } +} + +$pgtitle = array("Services", "Acme", "Accountkeys"); +include("head.inc"); +if ($input_errors) { + print_input_errors($input_errors); +} +if ($savemsg) { + print_info_box($savemsg); +} + +/*$display_apply = file_exists($d_acmeconfdirty_path) ? "" : "none"; +echo "
"; +print_apply_box(sprintf(gettext("The configuration has been changed.%sYou must apply the changes in order for them to take effect."), "
")); +echo "
"; +*/ +?> + + + +
+
+
+

Certificates

+
+
+ + + + + + + + + + + + + onClick="fr_toggle('')" ondblclick="document.location='acme_accountkeys_edit.php?id=';"> + + + + + + + +
NameDescriptionCAActions
+ + "> + + + + + + + + + + + + + + + + + +
+
+
+ +
+ + + +$fields_domains=array(); +$fields_domains[0]['name']="status"; +$fields_domains[0]['columnheader']="Mode"; +$fields_domains[0]['colwidth']="5%"; +$fields_domains[0]['type']="select"; +$fields_domains[0]['size']="70px"; +$fields_domains[0]['items']=&$a_enabledisable; +$fields_domains[1]['name']="name"; +$fields_domains[1]['columnheader']="Domainname"; +$fields_domains[1]['colwidth']="20%"; +$fields_domains[1]['type']="textbox"; +$fields_domains[1]['size']="30"; +$fields_domains[2]['name']="method"; +$fields_domains[2]['columnheader']="Method"; +$fields_domains[2]['colwidth']="15%"; +$fields_domains[2]['type']="select"; +$fields_domains[2]['size']="100px"; +$fields_domains[2]['items']=&$acme_domain_validation_method; + +$fields_domains_details=array(); +foreach($acme_domain_validation_method as $key => $action) { + if (is_array($action['fields'])) { + foreach($action['fields'] as $field) { + $item = $field; + $name = $key . $item['name']; + $item['name'] = $name; + //$item['customdrawcell'] = customdrawcell_actions; + $fields_domains_details[$name] = $item; + } + } +} +$domainslist = new HtmlList("table_domains", $fields_domains); +$domainslist->keyfield = "name"; +$domainslist->fields_details = $fields_domains_details; +$domainslist->editmode = $isnewitem; + +// + +// +$fields_actions=array(); +$fields_actions[0]['name']="status"; +$fields_actions[0]['columnheader']="Mode"; +$fields_actions[0]['colwidth']="5%"; +$fields_actions[0]['type']="select"; +$fields_actions[0]['size']="70px"; +$fields_actions[0]['items']=&$a_enabledisable; +$fields_actions[1]['name']="command"; +$fields_actions[1]['columnheader']="Command"; +$fields_actions[1]['colwidth']="20%"; +$fields_actions[1]['type']="textbox"; +$fields_actions[1]['size']="30"; +$fields_actions[2]['name']="method"; +$fields_actions[2]['columnheader']="Method"; +$fields_actions[2]['colwidth']="15%"; +$fields_actions[2]['type']="select"; +$fields_actions[2]['size']="100px"; +$fields_actions[2]['items']=&$acme_newcertificateactions; + +$fields_actions_details=array(); +foreach($acme_newcertificateactions as $key => $action) { + if (is_array($action['fields'])) { + foreach($action['fields'] as $field) { + $item = $field; + $name = $key . $item['name']; + $item['name'] = $name; + //$item['customdrawcell'] = customdrawcell_actions; + $fields_actions_details[$name] = $item; + } + } +} +$actionslist = new HtmlList("table_actions", $fields_actions); +$actionslist->keyfield = "name"; +//$actionslist->fields_details = $fields_actions_details; +$actionslist->editmode = $isnewitem; + +// + +function customdrawcell_actions($object, $item, $itemvalue, $editable, $itemname, $counter) { + if ($editable) { + $object->acme_htmllist_drawcell($item, $itemvalue, $editable, $itemname, $counter); + } else { + echo $itemvalue; + } +} + +if (isset($id) && $a_accountkeys[$id]) { + $a_domains = &$a_accountkeys[$id]['a_domainlist']['item']; + $a_actions = &$a_accountkeys[$id]['a_actions']['item']; + + $pconfig["lastrenewal"] = $a_accountkeys[$id]["lastrenewal"]; + $pconfig['accountkey'] = base64_decode($a_accountkeys[$id]['accountkey']); + foreach($simplefields as $stat) { + $pconfig[$stat] = $a_accountkeys[$id][$stat]; + } + + $a_errorfiles = &$a_accountkeys[$id]['errorfiles']['item']; + if (!is_array($a_errorfiles)) { + $a_errorfiles = array(); + } +} + +if (isset($_GET['dup'])) { + unset($id); + $pconfig['name'] .= "-copy"; +} +$changedesc = "Services: Acme: Certificate options: "; +$changecount = 0; + +if ($_POST) { + $changecount++; + + unset($input_errors); + $pconfig = $_POST; + + $reqdfields = explode(" ", "name"); + $reqdfieldsn = explode(",", "Name"); + + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + + if ($_POST['stats_enabled']) { + $reqdfields = explode(" ", "name stats_uri"); + $reqdfieldsn = explode(",", "Name,Stats Uri"); + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + if ($_POST['stats_username']) { + $reqdfields = explode(" ", "stats_password stats_realm"); + $reqdfieldsn = explode(",", "Stats Password,Stats Realm"); + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + } + } + + /*if (preg_match("/[^a-zA-Z0-9\.\-_]/", $_POST['name'])) { + $input_errors[] = "The field 'Name' contains invalid characters."; + } + if ($_POST['checkinter'] !== "" && !is_numeric($_POST['checkinter'])) { + $input_errors[] = "The field 'Check frequency' value is not a number."; + } + if ($_POST['connection_timeout'] !== "" && !is_numeric($_POST['connection_timeout'])) { + $input_errors[] = "The field 'Connection timeout' value is not a number."; + } + + if ($_POST['server_timeout'] !== "" && !is_numeric($_POST['server_timeout'])) { + $input_errors[] = "The field 'Server timeout' value is not a number."; + } + + if ($_POST['retries'] !== "" && !is_numeric($_POST['retries'])) { + $input_errors[] = "The field 'Retries' value is not a number."; + } + + // the colon ":" is invalid in the username, other than that pretty much any character can be used. + if (preg_match("/[^a-zA-Z0-9!-\/;-~ ]/", $_POST['stats_username'])) { + $input_errors[] = "The field 'Stats Username' contains invalid characters."; + } + + // the colon ":" can also be used in the password + if (preg_match("/[^a-zA-Z0-9!-~ ]/", $_POST['stats_password'])) { + $input_errors[] = "The field 'Stats Password' contains invalid characters."; + } + + if (preg_match("/[^a-zA-Z0-9\-_]/", $_POST['stats_node'])) { + $input_errors[] = "The field 'Stats Node' contains invalid characters. Should be a string with digits(0-9), letters(A-Z, a-z), hyphen(-) or underscode(_)"; + }*/ + + /* Ensure that our pool names are unique */ + for ($i=0; isset($config['installedpackages']['acme']['accountkeys']['item'][$i]); $i++) { + if (($_POST['name'] == $config['installedpackages']['acme']['accountkeys']['item'][$i]['name']) && ($i != $id)) { + $input_errors[] = "This pool name has already been used. Pool names must be unique."; + } + } + $a_domains = $domainslist->acme_htmllist_get_values(); + foreach($a_domains as $server){ + $domain_name = $server['name']; + if (!is_hostname($domain_name)) { + $input_errors[] = "The field 'Domainname' does not contain a valid hostname."; + } + } + $a_actions = $actionslist->acme_htmllist_get_values(); + + $accountkey = array(); + if(isset($id) && $a_accountkeys[$id]) { + $accountkey = $a_accountkeys[$id]; + } + +// echo "newname id:$id"; + if (!empty($accountkey['name']) && ($accountkey['name'] != $_POST['name'])) { + //old $accountkey['name'] can be empty if a new or cloned item is saved, nothing should be renamed then + // name changed: + $oldvalue = $accountkey['name']; + $newvalue = $_POST['name']; + + $a_accountkeys = &$config['installedpackages']['acme']['accountkeys']['item']; + if (!is_array($a_accountkeys)) { + $a_accountkeys = array(); + } + } + + if($accountkey['name'] != "") { + $changedesc .= " modified pool: '{$accountkey['name']}'"; + } + $accountkey['a_domainlist']['item'] = $a_domains; + $accountkey['a_actionlist']['item'] = $a_actions; + + $accountkey['accountkey'] = base64_encode($_POST['accountkey']); + global $simplefields; + foreach($simplefields as $stat) { + update_if_changed($stat, $accountkey[$stat], $_POST[$stat]); + } + + if (isset($id) && $a_accountkeys[$id]) { + $a_accountkeys[$id] = $accountkey; + } else { + $a_accountkeys[] = $accountkey; + } + if (!isset($input_errors)) { + if ($changecount > 0) { + touch($d_acmeconfdirty_path); + write_config($changedesc); + } + echo "
";
+		//print_r($config['installedpackages']['acme']);
+		header("Location: acme_accountkeys.php");
+		exit;
+	}
+}
+
+//$closehead = false;
+$pgtitle = array("Services", "Acme", "Certificate options: Edit");
+include("head.inc");
+display_top_tabs_active($acme_tab_array['acme'], "backend");
+
+// 'processing' done, make all simple fields usable in html.
+foreach($simplefields as $field){
+	$pconfig[$field] = htmlspecialchars($pconfig[$field]);
+}
+
+?>
+
+addInput(new \Form_Input('name', 'Name', 'text', $pconfig['name']
+))->setHelp('');
+$section->addInput(new \Form_Input('desc', 'Description', 'text', $pconfig['desc']));
+
+$section->addInput(new \Form_Select(
+	'acmeserver',
+	'Acme Server',
+	$pconfig['acmeserver'],
+	form_keyvalue_array($a_acmeserver)
+));
+
+$section->addInput(new \Form_Textarea(
+	'accountkey',
+	'Account key',
+	$pconfig['accountkey']
+))->setNoWrap();
+
+$section->addInput(new \Form_StaticText(
+	'', 
+	""
+		. " Create new account key"
+));
+
+$section->addInput(new \Form_StaticText(
+	'Acme account registration',
+	""
+		. " Register acme account key"
+))->setHelp("Before using a accountkey it must first be registered at the chosen CA.");
+
+$form->add($section);
+
+print $form;
+?>	
+	
+	
+	
+
+ + + + + $before and not selected */ + for ($i = $before+1; $i < count($items); $i++) { + if (!in_array($i, $selected)) { + $a_new[] = $items[$i]; + } + } + if (count($a_new) > 0) { + $items = $a_new; + } +} + +if($_POST['action'] == "toggle") { + $id = $_POST['id']; + echo "$id|"; + if (isset($a_certifcates[get_certificate_id($id)])) { + $frontent = &$a_certifcates[get_certificate_id($id)]; + if ($frontent['status'] != "disabled"){ + $frontent['status'] = 'disabled'; + echo "0|"; + }else{ + $frontent['status'] = 'active'; + echo "1|"; + } + $changedesc .= " set frontend '$id' status to: {$frontent['status']}"; + + touch($d_acmeconfdirty_path); + write_config($changedesc); + } + echo "ok|"; + exit; +} +if($_POST['action'] == "renew") { + $id = $_POST['id']; + echo $id . "\n"; + if (isset($a_certifcates[get_certificate_id($id)])) { + renew_certificate($id, true); + } + exit; +} + +if ($_POST) { + $pconfig = $_POST; + + if ($_POST['del_x']) { + /* delete selected rules */ + $deleted = false; + if (is_array($_POST['rule']) && count($_POST['rule'])) { + $selected = array(); + foreach($_POST['rule'] as $selection) { + $selected[] = get_certificate_id($selection); + } + foreach ($selected as $itemnr) { + unset($a_certifcates[$itemnr]); + $deleted = true; + } + if ($deleted) { + if (write_config("Acme, deleting certificate(s)")) { + //mark_subsystem_dirty('filter'); + touch($d_acmeconfdirty_path); + } + } + header("Location: acme_certificates.php"); + exit; + } + } else { + + // from '\src\usr\local\www\vpn_ipsec.php' + /* yuck - IE won't send value attributes for image buttons, while Mozilla does - so we use .x/.y to find move button clicks instead... */ + // TODO: this. is. nasty. + unset($delbtn, $delbtnp2, $movebtn, $movebtnp2, $togglebtn, $togglebtnp2); + foreach ($_POST as $pn => $pd) { + if (preg_match("/move_(.+)/", $pn, $matches)) { + $movebtn = $matches[1]; + } + } + // + + /* move selected p1 entries before this */ + if (isset($movebtn) && is_array($_POST['rule']) && count($_POST['rule'])) { + $moveto = get_frontend_id($movebtn); + $selected = array(); + foreach($_POST['rule'] as $selection) { + $selected[] = get_frontend_id($selection); + } + array_moveitemsbefore($a_certifcates, $moveto, $selected); + + touch($d_acmeconfdirty_path); + write_config($changedesc); + } + } +} + +if ($_GET['act'] == "del") { + $id = $_GET['id']; + $id = get_certificate_id($id); + if (isset($a_certifcates[$id])) { + if (!$input_errors) { + unset($a_certifcates[$id]); + $changedesc .= " Frontend delete"; + write_config($changedesc); + touch($d_acmeconfdirty_path); + } + header("Location: acme_certificates.php"); + exit; + } +} + +$pgtitle = array("Services", "Acme", "Certificates"); +include("head.inc"); +if ($input_errors) { + print_input_errors($input_errors); +} +if ($savemsg) { + print_info_box($savemsg); +} + +/*$display_apply = file_exists($d_acmeconfdirty_path) ? "" : "none"; +echo "
"; +print_apply_box(sprintf(gettext("The configuration has been changed.%sYou must apply the changes in order for them to take effect."), "
")); +echo "
"; +*/ +?> + + + +
+
+
+

Certificates

+
+
+ + + + + + + + + + + + + + + + onClick="fr_toggle('')" ondblclick="document.location='acme_certificates_edit.php?id=';" > + + + + + + + + + + +
OnNameDescriptionAccountLast renewedRenewActions
+ + "> + + + + + + + + + + + + + + + + Renew + + + + + + + + + + + + +
+
+
+ +
+ + + +$fields_domains=array(); +$fields_domains[0]['name']="status"; +$fields_domains[0]['columnheader']="Mode"; +$fields_domains[0]['colwidth']="5%"; +$fields_domains[0]['type']="select"; +$fields_domains[0]['size']="70px"; +$fields_domains[0]['items']=&$a_enabledisable; +$fields_domains[1]['name']="name"; +$fields_domains[1]['columnheader']="Domainname"; +$fields_domains[1]['colwidth']="20%"; +$fields_domains[1]['type']="textbox"; +$fields_domains[1]['size']="30"; +$fields_domains[2]['name']="method"; +$fields_domains[2]['columnheader']="Method"; +$fields_domains[2]['colwidth']="15%"; +$fields_domains[2]['type']="select"; +$fields_domains[2]['size']="100px"; +$fields_domains[2]['items']=&$acme_domain_validation_method; + +$fields_domains_details=array(); +foreach($acme_domain_validation_method as $key => $action) { + if (is_array($action['fields'])) { + foreach($action['fields'] as $field) { + $item = $field; + $name = $key . $item['name']; + $item['name'] = $name; + //$item['customdrawcell'] = customdrawcell_actions; + $fields_domains_details[$name] = $item; + } + } +} +$domainslist = new HtmlList("table_domains", $fields_domains); +$domainslist->keyfield = "name"; +$domainslist->fields_details = $fields_domains_details; +$domainslist->editmode = $isnewitem; + +// + +// +$fields_actions=array(); +$fields_actions[0]['name']="status"; +$fields_actions[0]['columnheader']="Mode"; +$fields_actions[0]['colwidth']="5%"; +$fields_actions[0]['type']="select"; +$fields_actions[0]['size']="70px"; +$fields_actions[0]['items']=&$a_enabledisable; +$fields_actions[1]['name']="command"; +$fields_actions[1]['columnheader']="Command"; +$fields_actions[1]['colwidth']="20%"; +$fields_actions[1]['type']="textbox"; +$fields_actions[1]['size']="30"; +$fields_actions[2]['name']="method"; +$fields_actions[2]['columnheader']="Method"; +$fields_actions[2]['colwidth']="15%"; +$fields_actions[2]['type']="select"; +$fields_actions[2]['size']="100px"; +$fields_actions[2]['items']=&$acme_newcertificateactions; + +$fields_actions_details=array(); +foreach($acme_newcertificateactions as $key => $action) { + if (is_array($action['fields'])) { + foreach($action['fields'] as $field) { + $item = $field; + $name = $key . $item['name']; + $item['name'] = $name; + //$item['customdrawcell'] = customdrawcell_actions; + $fields_actions_details[$name] = $item; + } + } +} +$actionslist = new HtmlList("table_actions", $fields_actions); +$actionslist->keyfield = "name"; +//$actionslist->fields_details = $fields_actions_details; +$actionslist->editmode = $isnewitem; + +// + +function customdrawcell_actions($object, $item, $itemvalue, $editable, $itemname, $counter) { + if ($editable) { + $object->acme_htmllist_drawcell($item, $itemvalue, $editable, $itemname, $counter); + } else { + echo $itemvalue; + } +} + +if (isset($id) && $a_certificates[$id]) { + $a_domains = &$a_certificates[$id]['a_domainlist']['item']; + $a_actions = &$a_certificates[$id]['a_actions']['item']; + + $pconfig["lastrenewal"] = $a_certificates[$id]["lastrenewal"]; + foreach($simplefields as $stat) { + $pconfig[$stat] = $a_certificates[$id][$stat]; + } + + $a_errorfiles = &$a_certificates[$id]['errorfiles']['item']; + if (!is_array($a_errorfiles)) { + $a_errorfiles = array(); + } +} + +if (isset($_GET['dup'])) { + unset($id); + $pconfig['name'] .= "-copy"; +} +$changedesc = "Services: Acme: Certificate options: "; +$changecount = 0; + +if ($_POST) { + $changecount++; + + unset($input_errors); + $pconfig = $_POST; + + $reqdfields = explode(" ", "name"); + $reqdfieldsn = explode(",", "Name"); + + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + + if ($_POST['stats_enabled']) { + $reqdfields = explode(" ", "name stats_uri"); + $reqdfieldsn = explode(",", "Name,Stats Uri"); + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + if ($_POST['stats_username']) { + $reqdfields = explode(" ", "stats_password stats_realm"); + $reqdfieldsn = explode(",", "Stats Password,Stats Realm"); + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + } + } + + /* Ensure that our pool names are unique */ + for ($i=0; isset($config['installedpackages']['acme']['certificates']['item'][$i]); $i++) { + if (($_POST['name'] == $config['installedpackages']['acme']['certificates']['item'][$i]['name']) && ($i != $id)) { + $input_errors[] = "This pool name has already been used. Pool names must be unique."; + } + } + $a_domains = $domainslist->acme_htmllist_get_values(); + foreach($a_domains as $server){ + $domain_name = $server['name']; + if (!is_hostname($domain_name)) { + $input_errors[] = "The field 'Domainname' does not contain a valid hostname."; + } + } + $a_actions = $actionslist->acme_htmllist_get_values(); + + $certificate = array(); + if(isset($id) && $a_certificates[$id]) { + $certificate = $a_certificates[$id]; + } + +// echo "newname id:$id"; + if (!empty($certificate['name']) && ($certificate['name'] != $_POST['name'])) { + //old $certificate['name'] can be empty if a new or cloned item is saved, nothing should be renamed then + // name changed: + $oldvalue = $certificate['name']; + $newvalue = $_POST['name']; + + $a_certificates = &$config['installedpackages']['acme']['certificates']['item']; + if (!is_array($a_certificates)) { + $a_certificates = array(); + } + } + + if($certificate['name'] != "") { + $changedesc .= " modified pool: '{$certificate['name']}'"; + } + $certificate['a_domainlist']['item'] = $a_domains; + $certificate['a_actionlist']['item'] = $a_actions; + + global $simplefields; + foreach($simplefields as $stat) { + update_if_changed($stat, $certificate[$stat], $_POST[$stat]); + } + if (isset($id) && $a_certificates[$id]) { + $a_certificates[$id] = $certificate; + } else { + $a_certificates[] = $certificate; + } + if (!isset($input_errors)) { + if ($changecount > 0) { + touch($d_acmeconfdirty_path); + write_config($changedesc); + } + header("Location: acme_certificates.php"); + exit; + } +} + +$closehead = false; +$pgtitle = array("Services", "Acme", "Certificate options: Edit"); +include("head.inc"); +display_top_tabs_active($acme_tab_array['acme'], "backend"); + +// 'processing' done, make all simple fields usable in html. +foreach($simplefields as $field){ + $pconfig[$field] = htmlspecialchars($pconfig[$field]); +} + +?> + + +addInput(new \Form_Input('name', 'Name', 'text', $pconfig['name'] +))->setHelp(''); +$section->addInput(new \Form_Input('desc', 'Description', 'text', $pconfig['desc'])); +$activedisable = array(); +$activedisable['active'] = "Active"; +$activedisable['disable'] = "Disable"; +$section->addInput(new \Form_Select( + 'status', + 'Status', + $pconfig['status'], + $activedisable +)); +$a_accountkeys = &$config['installedpackages']['acme']['accountkeys']['item']; +//$a_frontendmode['http'] = array('name' => "http / https(offloading)", 'shortname' => "http/https"); +$section->addInput(new \Form_Select( + 'acmeaccount', + 'Acme Account', + $pconfig['acmeaccount'], + form_name_array($a_accountkeys) +)); + +$section->addInput(new \Form_StaticText( + 'Domain SAN list', + "List all domain names that should be included in the certificate here". +$domainslist->Draw($a_domains) +)); + +$section->addInput(new \Form_StaticText( + 'Actions list', + "Used to restart webserver provesses after certificates have been renewed". + $actionslist->Draw($a_actions) +)); + +$section->addInput(new \Form_Input('', 'Last renewal', 'text', + date('d-m-Y H:i:s', $pconfig['lastrenewal']) +))->setReadonly()->setHelp('The last time this certificate was renewed'); + +$section->addInput(new \Form_Input('renewafter', 'Certificate renewal after', 'text', $pconfig['renewafter'] +))->setHelp('After how many days the certicicate should be renewed, defaults to 60'); + +$form->add($section); + +print $form; +?> + + + +
+ + + +addInput(new \Form_Checkbox( + 'enable', + '', + 'Enable Acme client renewal job', + $pconfig['enable'] +)); + +$form->add($section); + +print $form; + +function group_input_with_text($name, $title, $type = 'text', $value = null, array $attributes = array(), $righttext = "") +{ + $group = new \Form_Group($title); + $group->add(new \Form_Input( + $name, + '', + $type, + $value, + $attributes + ))->setWidth(2); + + $group->add(new \Form_StaticText( + '', + $righttext + )); + return $group; +} + +include("foot.inc"); \ No newline at end of file diff --git a/security/pfSense-pkg-acme/pkg-descr b/security/pfSense-pkg-acme/pkg-descr new file mode 100644 index 000000000000..d30f517ea454 --- /dev/null +++ b/security/pfSense-pkg-acme/pkg-descr @@ -0,0 +1,3 @@ +Automated Certificate Management Environment, for automated use of LetsEncrypt certificates. + +WWW: https://doc.pfsense.org/index.php/pfsense-pkg-acme diff --git a/security/pfSense-pkg-acme/pkg-plist b/security/pfSense-pkg-acme/pkg-plist new file mode 100644 index 000000000000..e07ad7fee705 --- /dev/null +++ b/security/pfSense-pkg-acme/pkg-plist @@ -0,0 +1,15 @@ +pkg/acme.xml +pkg/acme/acme.inc +pkg/acme/acme_gui.inc +pkg/acme/acme_htmllist.inc +pkg/acme/acme_utils.inc +pkg/acme/lescript.inc +pkg/acme/pkg_acme_tabs.inc +www/acme/acme_accountkeys.php +www/acme/acme_accountkeys_edit.php +www/acme/acme_certificates.php +www/acme/acme_certificates_edit.php +www/acme/acme_generalsettings.php +/etc/inc/priv/acme.priv.inc +%%DATADIR%%/info.xml +@dir /etc/inc/priv From 18573028cb3faa6c60876691c71e910537dbef91 Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Fri, 1 Apr 2016 01:55:29 +0200 Subject: [PATCH 02/14] acme, fix make / install issue --- security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml | 4 ++-- .../files/usr/local/share/pfSense-pkg-acme/info.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml index c3d7b68cbd12..f93eb7a2a60d 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml @@ -62,9 +62,9 @@ installedpackages->acme->config - acme_custom_php_install_command(); + pfsense_pkg\acme\acme_custom_php_install_command(); - acme_custom_php_deinstall_command(); + pfsense_pkg\acme\acme_custom_php_deinstall_command(); diff --git a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml index 90398477ded1..ef0884dcc225 100644 --- a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml +++ b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml @@ -1,7 +1,7 @@ - Acme + acme https://doc.pfsense.org/index.php/pfsense-pkg-acme https://letsencrypt.org/ From 44eb3c224ba9684133cc72c7645e90ca99044556 Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Fri, 8 Apr 2016 23:20:28 +0200 Subject: [PATCH 03/14] acme, new methods sftp/ftps for deploying webroot challenges to remote webservers, some bugfixes --- security/pfSense-pkg-acme/Makefile | 4 +- .../files/usr/local/pkg/acme.xml | 4 +- .../files/usr/local/pkg/acme/acme.inc | 95 ++++++----- .../local/pkg/acme/acme_serverconnectors.inc | 150 ++++++++++++++++++ .../files/usr/local/pkg/acme/acme_utils.inc | 2 + .../files/usr/local/pkg/acme/lescript.inc | 68 ++++---- .../usr/local/share/pfSense-pkg-acme/info.xml | 2 +- .../usr/local/www/acme/acme_accountkeys.php | 75 ++++----- .../usr/local/www/acme/acme_certificates.php | 4 +- security/pfSense-pkg-acme/pkg-plist | 1 + 10 files changed, 272 insertions(+), 133 deletions(-) create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_serverconnectors.inc diff --git a/security/pfSense-pkg-acme/Makefile b/security/pfSense-pkg-acme/Makefile index 1d0496d52bb1..fc9aeeed1ffa 100644 --- a/security/pfSense-pkg-acme/Makefile +++ b/security/pfSense-pkg-acme/Makefile @@ -10,7 +10,7 @@ EXTRACT_ONLY= # empty MAINTAINER= PiBa-NL COMMENT= pfSense package acme -RUN_DEPENDS= +RUN_DEPENDS= ${PORTSDIR}/ftp/php56-ftp CONFLICTS= @@ -38,6 +38,8 @@ do-install: ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_htmllist.inc \ ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_serverconnectors.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_utils.inc \ ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/lescript.inc \ diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml index f93eb7a2a60d..c3d7b68cbd12 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml @@ -62,9 +62,9 @@ installedpackages->acme->config - pfsense_pkg\acme\acme_custom_php_install_command(); + acme_custom_php_install_command(); - pfsense_pkg\acme\acme_custom_php_deinstall_command(); + acme_custom_php_deinstall_command(); diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index 246a79a88591..4198dbe22e18 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -26,7 +26,18 @@ POSSIBILITY OF SUCH DAMAGE. */ -namespace pfsense_pkg\acme; +namespace { // global code + function acme_plugin_certificates($pluginparams) { + $result = array(); + if ($pluginparams['type'] == 'certificates' && $pluginparams['event'] == 'used_certificates') { + $result['pkgname'] = "Acme"; + $result['certificatelist'] = array(); + } + return $result; + } +} + +namespace pfsense_pkg\acme { /* include all configuration functions */ require_once("functions.inc"); @@ -35,10 +46,11 @@ require_once("notices.inc"); require_once("util.inc"); require_once("acme_utils.inc"); require_once("lescript.inc"); +require_once("acme/acme_serverconnectors.inc"); $d_acmeconfdirty_path = $g['varrun_path'] . "/acme.conf.dirty"; #region global array item definitions.. -// +// $a_enabledisable = array(); $a_enabledisable['enable'] = array('name' => 'Enabled'); @@ -61,15 +73,11 @@ $acme_domain_validation_method['webroot'] = array(name => "local webroot folder" 'description' =>"Folder the acme challenge response is written to for example: /usr/local/www/.well-known/acme-challenge/" ) )); - -/* -$acme_domain_validation_method['ftpwebroot'] = array(name => "ftpwebroot", +$acme_domain_validation_method['ftpwebroot'] = array(name => "FTP webroot", 'fields' => array( - 'server' => array('name'=>"ftpserver",'columnheader'=>"Server",'type'=>"textbox",'size'=>"50", - 'description' =>"Hostname of FTP server to connect to for example ftp://www.webserver.tld" - ), - 'method' => array('name'=>"method",'columnheader'=>"Method",'type'=>"textbox",'size'=>"50", - 'description' =>"Method (scp/sftp/ftp/other) to connect to the remote server" + 'ftpserver' => array('name'=>"ftpserver",'columnheader'=>"Server",'type'=>"textbox",'size'=>"50", + 'description' =>"Hostname of FTP server to connect to for example: ftps://www.webserver.tld " + . "currently ftps(passive) and sftp are supported." ), 'username' => array('name'=>"username",'columnheader'=>"Username",'type'=>"textbox",'size'=>"50", 'description' =>"Username for the remote server" @@ -81,13 +89,12 @@ $acme_domain_validation_method['ftpwebroot'] = array(name => "ftpwebroot", 'description' =>"Folder the acme challenge response is written to for default: /.well-known/acme-challenge/" ) )); -$acme_domain_validation_method['http-post'] = array(name => "http-post", +/*$acme_domain_validation_method['http-post'] = array(name => "http-post", 'fields' => array( 'url' => array('name'=>"url",'columnheader'=>"Url",'type'=>"textbox",'size'=>"50", 'description' =>"Url the challenge file is posted to, the webserver there must store and reply to the request when the acme servers perform the request for the file from /.well-known/acme-challenge/" ) - )); -*/ + ));*/ $acme_newcertificateactions = array(); $acme_newcertificateactions['shellcommand'] = array(name => "shell command"); @@ -121,7 +128,7 @@ function acme_custom_php_deinstall_command() { } function acme_custom_php_install_command() { - global $g, $config, $static_output; + global $static_output; $static_output .= "Acme, running acme_custom_php_install_command()\n"; update_output_window($static_output); @@ -159,7 +166,7 @@ EOD; function get_itembyname($a_array, $name) { $i = 0; if (is_array($a_array)) { - foreach ($a_array as $key => $item) { + foreach ($a_array as $item) { if ($item['name'] == $name) { return $i; } @@ -186,7 +193,7 @@ function get_certificate_id($name) { $a_certificates = &$config['installedpackages']['acme']['certificates']['item']; $i = 0; if (is_array($a_certificates)) { - foreach ($a_certificates as $key => $certificate) { + foreach ($a_certificates as $certificate) { if ($certificate['name'] == $name) { return $i; } @@ -206,7 +213,7 @@ function & get_certificate($name) { return null; } - function createAcmeAccountKey(){ + function createAcmeAccountKey() { $certificatename = "acme_account_key"; $cert = lookup_cert_by_name($certificatename); if (!is_array($cert)) { @@ -223,8 +230,7 @@ function & get_certificate($name) { write_config($changedesc); } } - function generateAccountKey() - { + function generateAccountKey() { $res = openssl_pkey_new(array( "private_key_type" => OPENSSL_KEYTYPE_RSA, "private_key_bits" => 4096, @@ -240,7 +246,7 @@ function & get_certificate($name) { $le = \Analogic\ACME\Lescript::createWithCustomEvents($ca, $logger); return $le; } - function renew_all_certificates(){ + function renew_all_certificates() { global $config; $a_global = &$config['installedpackages']['acme']; @@ -300,42 +306,53 @@ function & get_certificate($name) { } } syslog(LOG_NOTICE, "Acme, certificate renewed: {$id}"); - } } class acme_handler { public $path = ""; public $certificateinfo = null; + private $ftp = null; function chalenge_response_put($domain, $token, $payload){ + echo "\nchalenge_response_put\n"; foreach($this->certificateinfo['a_domainlist']['item'] as $domainitem) { if($domainitem['name'] == $domain){ $domain_info = $domainitem; } } - //echo "





domain_info({$domain})
"; - //print_r($domain_info); if ($domain_info['method'] == 'webroot') { - //echo "
USING WEBROOT"; $directory = $domain_info['webrootfolder']; if(!file_exists($directory) && !@mkdir($directory, 0755, true)) { throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); } $tokenPath = $directory . "/" . $token; - //echo "
Saving token in:".$tokenPath; file_put_contents($tokenPath, $payload); } + if ($domain_info['method'] == 'ftpwebroot') { + echo "FTP\n"; + $this->ftp = new FTPConnection($domain_info['ftpwebrootftpserver']); + $this->ftp->login($domain_info['ftpwebrootusername'], $domain_info['ftpwebrootpassword']); + $directory = $domain_info['ftpwebrootfolder']; + $tokenPath = $directory . "/" . $token; + $this->ftp->mkdir($directory); + $this->ftp->uploadData($payload, $tokenPath); + } } - function chalenge_response_cleanup($domain, $token){ + function chalenge_response_cleanup($domain, $token) { foreach($this->certificateinfo['a_domainlist']['item'] as $domainitem) { if($domainitem['name'] == $domain){ $domain_info = $domainitem; } } if ($domain_info['method'] == 'webroot') { - @unlink($tokenPath); + $tokenfile = $domain_info['webrootfolder'] . "/" . $token; + @unlink($tokenfile); + } + if ($domain_info['method'] == 'ftpwebroot') { + $tokenfile = $domain_info['ftpwebrootfolder'] . "/" . $token; + $this->ftp->deleteFile($tokenfile); } } - function getCertificatePSK(){ + function getCertificatePSK() { $certificatename = "acme_cert_" . $this->certificateinfo['name']; $cert = lookup_cert_by_name($certificatename); if (!is_array($cert)) { @@ -345,14 +362,12 @@ function & get_certificate($name) { $cert['refid'] = uniqid(); $cert['descr'] = $certificatename; $accountkey = generateAccountKey(); - //cert_import($cert, $accountkey['crt'], $accountkey['prv']); - //$cert['crt'] = base64_encode($crt_str); $cert['prv'] = base64_encode($accountkey['prv']); $a_cert[] = $cert; } return base64_decode($cert['prv']); } - function storeCertificate($certificates){ + function storeCertificate($certificates) { $certificatename = "acme_cert_" . $this->certificateinfo['name']; global $config; if (is_array($config['cert'])) { @@ -367,7 +382,7 @@ function & get_certificate($name) { } } } - // store chain to.?. + //TODO: store chain to.?. //$cert = lookup_cert_by_name($certificatename); //if (is_array($cert)) { @@ -381,25 +396,17 @@ function & get_certificate($name) { } function registerAcmeAccountKey($ca, $key) { - //public $ca = 'https://acme-v01.api.letsencrypt.org'; - //$ca = 'https://acme-staging.api.letsencrypt.org'; // testing - - //$certificatename = "acme_account_key"; - //$cert = lookup_cert_by_name($certificatename); - //$key = $cert['prv']; - - //echo "




ACME result:
"; $logger = new Logger(); $le = \Analogic\ACME\Lescript::createWithCustomEvents($ca, $logger); - //$le->setPrivateKey(base64_decode($key)); $le->setPrivateKey($key); $result = $le->postNewReg(); - //print_r($result); - //echo "
"; + return $result; } + class Logger { function __call($name, $arguments) { echo date('Y-m-d H:i:s')." [$name] ${arguments[0]}\n"; } } -?> + +} \ No newline at end of file diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_serverconnectors.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_serverconnectors.inc new file mode 100644 index 000000000000..85adbd69c311 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_serverconnectors.inc @@ -0,0 +1,150 @@ +scheme = parse_url($url, PHP_URL_SCHEME); + $host = parse_url($url, PHP_URL_HOST); + $port = parse_url($url, PHP_URL_PORT); + if ($port == 0) { + if ($this->scheme == "ftp") { + $port = 21; + } elseif ($this->scheme == "sftp") { + $port = 22; + } elseif ($this->scheme == "ftps") { + $port = 21; + } + } + if ($this->scheme == "ftp") { + $this->connection = ftp_connect($host, $port); + } elseif ($this->scheme == "sftp") { + $this->connection = ssh2_connect($host, $port); + } elseif ($this->scheme == "ftps") { + $this->connection = \ftp_ssl_connect($host, $port); + } + if (! $this->connection) { + throw new \Exception("Could not connect with {$this->scheme} to {$host} on port {$port}."); + } + } + + public function login($username, $password) + { + if ($this->scheme == "ftp" || $this->scheme == "ftps") { + if(!ftp_login($this->connection, $username, $password)){ + throw new \Exception("Could not authenticate with username {$username} and its password"); + } + } else { + if (!ssh2_auth_password($this->connection, $username, $password)) { + throw new \Exception("Could not authenticate with username {$username} and its password"); + } + $this->sftp = ssh2_sftp($this->connection); + if (! $this->sftp) { + throw new \Exception("Could not initialize SFTP subsystem."); + } + } + if ($this->scheme == "ftp" || $this->scheme == "ftps") { + if (!ftp_pasv($this->connection, true)) {//passive connection is usualy desired to pass through firewalls.. + throw new \Exception("Could not switch to PASSIVE ftp for: {$this->scheme} to {$host} on port {$port}."); + } + } + } + + /*public function uploadFile($local_file, $remote_file) + { + $data_to_send = file_get_contents($local_file); + if ($data_to_send === false) { + throw new \Exception("Could not open local file: {$local_file}."); + } + uploadData($data_to_send, $remote_file); + }*/ + + public function uploadData($data_to_send, $remote_file) { + if ($this->scheme == "ftp" || $this->scheme == "ftps") { + echo "\n upload:{$data_to_send} tofile: {$remote_file}"; + $tempHandle = fopen('php://temp', 'r+'); + fwrite($tempHandle, $data_to_send); + rewind($tempHandle); + if (!ftp_fput($this->connection, $remote_file, $tempHandle, FTP_ASCII)) { + throw new \Exception("Could not upload file: {$remote_file}"); + } + fclose($tempHandle); + } else { + $sftp = $this->sftp; + $stream = @fopen("ssh2.sftp://{$sftp}{$remote_file}", 'w'); + if (! $stream) { + throw new \Exception("Could not open file: {$remote_file}"); + } + if (fwrite($stream, $data_to_send) === false) { + throw new \Exception("Could not send data to file: $remote_file."); + } + fclose($stream); + } + } + + public function deleteFile($remote_file) { + if ($this->scheme == "ftp" || $this->scheme == "ftps") { + if (!ftp_delete($this->connection , $remote_file)) { + throw new \Exception("Could not delete file: $remote_file."); + } + } else { + $sftp = $this->sftp; + ssh2_sftp_unlink($sftp, $remote_file); + } + } + + public function mkdir($remote_dir, $recursive = true) { + if ($this->scheme == "ftp" || $this->scheme == "ftps") { + if ($remote_dir[0] == "/") { + $remote_dir = substr($remote_dir, 1); + } + if ($remote_dir[strlen($remote_dir)-1] == "/") { + $remote_dir = substr($remote_dir, 0, strlen($remote_dir)-1); + } + + $parts = explode('/',$remote_dir); // 2013/06/11/username + foreach($parts as $part){ + if(!@ftp_chdir($this->connection, $part)){ + ftp_mkdir($this->connection, $part); + ftp_chdir($this->connection, $part); + //ftp_chmod($ftpcon, 0777, $part); + } + } + } else { + $sftp = $this->sftp; + ssh2_sftp_mkdir($sftp, $remote_dir, 0777, $recursive); + } + } +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc index ebf3f7c8198a..ff0b376828b9 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc @@ -31,6 +31,8 @@ be moved to the general pfSense php library for possible easy use by other parts of pfSense */ +namespace pfsense_pkg\acme; + require_once("config.inc"); if(!function_exists('ifset')){ diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc index 149fef9999c9..b32f2957e6e7 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc @@ -141,50 +141,48 @@ class Lescript /*file_put_contents($tokenPath, $payload); chmod($tokenPath, 0644);*/ $this->callback->chalenge_response_put($domain, $challenge['token'], $payload); - - // 3. verification process itself - // ------------------------------- + // 3. verification process itself + // ------------------------------- - $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; + $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); + $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); - // simple self check - if($payload !== trim(@file_get_contents($uri))) { - throw new \RuntimeException("Please check $uri - token not available"); - } + // simple self check + if($payload !== trim(@file_get_contents($uri))) { + throw new \RuntimeException("Please check $uri - token not available"); + } - $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'] - ) - ); + $this->log("Sending request to challenge"); - // waiting loop - do { - if(empty($result['status']) || $result['status'] == "invalid") { - throw new \RuntimeException("Verification ended with error: ".json_encode($result)); - } - $ended = !($result['status'] === "pending"); + // send request to challenge + $result = $this->signedRequest( + $challenge['uri'], + array( + "resource" => "challenge", + "type" => "http-01", + "keyAuthorization" => $payload, + "token" => $challenge['token'] + ) + ); - if(!$ended) { - $this->log("Verification pending, sleeping 1s"); - sleep(1); - } + // waiting loop + do { + if(empty($result['status']) || $result['status'] == "invalid") { + throw new \RuntimeException("Verification ended with error: ".json_encode($result)); + } + $ended = !($result['status'] === "pending"); + + if(!$ended) { + $this->log("Verification pending, sleeping 1s"); + sleep(1); + } - $result = $this->client->get($location); + $result = $this->client->get($location); - } while (!$ended); + } while (!$ended); - $this->log("Verification ended with status: ${result['status']}"); - //@unlink($tokenPath); + $this->log("Verification ended with status: ${result['status']}"); $this->callback->chalenge_response_cleanup($domain, $challenge['token']); } diff --git a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml index ef0884dcc225..90398477ded1 100644 --- a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml +++ b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml @@ -1,7 +1,7 @@ - acme + Acme https://doc.pfsense.org/index.php/pfsense-pkg-acme https://letsencrypt.org/ diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php index bb842d96d324..48cca8dfd502 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php @@ -42,7 +42,7 @@ if (!is_array($config['installedpackages']['acme']['accountkeys']['item'])) { $config['installedpackages']['acme']['accountkeys']['item'] = array(); } -$a_certifcates = &$config['installedpackages']['acme']['accountkeys']['item']; +$a_accountkeys = &$config['installedpackages']['acme']['accountkeys']['item']; function array_moveitemsbefore(&$items, $before, $selected) { // generic function to move array items before the set item by their numeric indexes. @@ -98,14 +98,6 @@ function array_moveitemsbefore(&$items, $before, $selected) { echo "ok|"; exit; } -if($_POST['action'] == "renew") { - $id = $_POST['id']; - echo $id . "\n"; - if (isset($a_certifcates[get_certificate_id($id)])) { - renew_certificate($id, true); - } - exit; -} if ($_POST) { $pconfig = $_POST; @@ -121,19 +113,19 @@ function array_moveitemsbefore(&$items, $before, $selected) { if (is_array($_POST['rule']) && count($_POST['rule'])) { $selected = array(); foreach($_POST['rule'] as $selection) { - $selected[] = get_certificate_id($selection); + $selected[] = get_accountkey_id($selection); } foreach ($selected as $itemnr) { - unset($a_certifcates[$itemnr]); + unset($a_accountkeys[$itemnr]); $deleted = true; } if ($deleted) { - if (write_config("Acme, deleting certificate(s)")) { + if (write_config("Acme, deleting accountkey(s)")) { //mark_subsystem_dirty('filter'); touch($d_acmeconfdirty_path); } } - header("Location: acme_certificates.php"); + header("Location: acme_accountkeys.php"); exit; } } else { @@ -151,12 +143,12 @@ function array_moveitemsbefore(&$items, $before, $selected) { /* move selected p1 entries before this */ if (isset($movebtn) && is_array($_POST['rule']) && count($_POST['rule'])) { - $moveto = get_frontend_id($movebtn); + $moveto = get_accountkey_id($movebtn); $selected = array(); foreach($_POST['rule'] as $selection) { - $selected[] = get_frontend_id($selection); + $selected[] = get_accountkey_id($selection); } - array_moveitemsbefore($a_certifcates, $moveto, $selected); + array_moveitemsbefore($a_accountkeys, $moveto, $selected); touch($d_acmeconfdirty_path); write_config($changedesc); @@ -171,15 +163,15 @@ function array_moveitemsbefore(&$items, $before, $selected) { if ($_GET['act'] == "del") { $id = $_GET['id']; - $id = get_certificate_id($id); - if (isset($a_certifcates[$id])) { + $id = get_accountkey_id($id); + if (isset($a_accountkeys[$id])) { if (!$input_errors) { - unset($a_certifcates[$id]); - $changedesc .= " Frontend delete"; + unset($a_accountkeys[$id]); + $changedesc .= " Accountkey delete"; write_config($changedesc); touch($d_acmeconfdirty_path); } - header("Location: acme_certificates.php"); + header("Location: acme_accountkeys.php"); exit; } } @@ -210,10 +202,10 @@ function array_moveitemsbefore(&$items, $before, $selected) { -
+
-

Certificates

+

Account keys

@@ -228,32 +220,32 @@ function array_moveitemsbefore(&$items, $before, $selected) { - onClick="fr_toggle('')" ondblclick="document.location='acme_accountkeys_edit.php?id=';"> + onClick="fr_toggle('')" ondblclick="document.location='acme_accountkeys_edit.php?id=';"> @@ -309,19 +301,6 @@ function js_callback(req_content) { } } -function renewcertificate($id) { - $('#'+"btnrenewicon_"+$id).removeClass("fa-check").addClass("fa-cog fa-spin"); - - ajaxRequest = $.ajax({ - url: "", - type: "post", - data: { id: $id, action: "renew"}, - success: function(data) { - js_callbackrenew(data); - } - }); -} - function togglerow($id) { ajaxRequest = $.ajax({ url: "", diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php index a2ece459d38a..6c28c4156190 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php @@ -146,10 +146,10 @@ function array_moveitemsbefore(&$items, $before, $selected) { /* move selected p1 entries before this */ if (isset($movebtn) && is_array($_POST['rule']) && count($_POST['rule'])) { - $moveto = get_frontend_id($movebtn); + $moveto = get_certificate_id($movebtn); $selected = array(); foreach($_POST['rule'] as $selection) { - $selected[] = get_frontend_id($selection); + $selected[] = get_certificate_id($selection); } array_moveitemsbefore($a_certifcates, $moveto, $selected); diff --git a/security/pfSense-pkg-acme/pkg-plist b/security/pfSense-pkg-acme/pkg-plist index e07ad7fee705..d8eefdc8f85f 100644 --- a/security/pfSense-pkg-acme/pkg-plist +++ b/security/pfSense-pkg-acme/pkg-plist @@ -2,6 +2,7 @@ pkg/acme.xml pkg/acme/acme.inc pkg/acme/acme_gui.inc pkg/acme/acme_htmllist.inc +pkg/acme/acme_serverconnectors.inc pkg/acme/acme_utils.inc pkg/acme/lescript.inc pkg/acme/pkg_acme_tabs.inc From 936725443476361e01bc98b36a397e5bd6ffa3eb Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Thu, 14 Apr 2016 20:00:26 +0200 Subject: [PATCH 04/14] acme, remove service definition --- security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml index c3d7b68cbd12..e0866d79d11a 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml @@ -51,10 +51,6 @@
Services
/acme/acme_certificates.php - - Acme - Automated Certificate Management Environment - plugin_certificates From 018348f536154de388fc73df93f44a4783422235 Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Mon, 25 Apr 2016 00:09:06 +0200 Subject: [PATCH 05/14] acme, fix installation names --- security/pfSense-pkg-acme/Makefile | 2 +- security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml | 4 ++-- .../files/usr/local/share/pfSense-pkg-acme/info.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/security/pfSense-pkg-acme/Makefile b/security/pfSense-pkg-acme/Makefile index fc9aeeed1ffa..101603459f6d 100644 --- a/security/pfSense-pkg-acme/Makefile +++ b/security/pfSense-pkg-acme/Makefile @@ -10,7 +10,7 @@ EXTRACT_ONLY= # empty MAINTAINER= PiBa-NL COMMENT= pfSense package acme -RUN_DEPENDS= ${PORTSDIR}/ftp/php56-ftp +# LIB_DEPENDS= ftp.so:ftp/php56-ftp CONFLICTS= diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml index e0866d79d11a..e5fea0a8709f 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme.xml @@ -58,9 +58,9 @@ installedpackages->acme->config - acme_custom_php_install_command(); + pfsense_pkg\acme\acme_custom_php_install_command(); - acme_custom_php_deinstall_command(); + pfsense_pkg\acme\acme_custom_php_deinstall_command(); diff --git a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml index 90398477ded1..ef0884dcc225 100644 --- a/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml +++ b/security/pfSense-pkg-acme/files/usr/local/share/pfSense-pkg-acme/info.xml @@ -1,7 +1,7 @@ - Acme + acme https://doc.pfsense.org/index.php/pfsense-pkg-acme https://letsencrypt.org/ From bfff56ec6a4e0d7dde6dce3f983b730201fce4ed Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Thu, 28 Apr 2016 23:17:50 +0200 Subject: [PATCH 06/14] acme, minor fixes + 'php command' ability --- security/pfSense-pkg-acme/Makefile | 2 +- .../files/usr/local/pkg/acme/acme.inc | 35 +++++++++++++------ .../usr/local/pkg/acme/acme_htmllist.inc | 1 + .../files/usr/local/pkg/acme/lescript.inc | 5 ++- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/security/pfSense-pkg-acme/Makefile b/security/pfSense-pkg-acme/Makefile index 101603459f6d..af2fec98e3df 100644 --- a/security/pfSense-pkg-acme/Makefile +++ b/security/pfSense-pkg-acme/Makefile @@ -10,7 +10,7 @@ EXTRACT_ONLY= # empty MAINTAINER= PiBa-NL COMMENT= pfSense package acme -# LIB_DEPENDS= ftp.so:ftp/php56-ftp +USE_PHP= ftp CONFLICTS= diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index 4198dbe22e18..27fdb339e4ab 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -98,7 +98,7 @@ $acme_domain_validation_method['ftpwebroot'] = array(name => "FTP webroot", $acme_newcertificateactions = array(); $acme_newcertificateactions['shellcommand'] = array(name => "shell command"); -//$acme_domain_validation_method['php command'] = array(name => "php command script"); +$acme_newcertificateactions['php_command'] = array(name => "php command script"); // #end @@ -249,10 +249,11 @@ function & get_certificate($name) { function renew_all_certificates() { global $config; $a_global = &$config['installedpackages']['acme']; - - foreach($a_global['certificates']['item'] as $certificate) { - echo "Checking if renewal is needed for: {$certificate['name']}\n"; - renew_certificate($certificate['name']); + if (is_array($a_global['certificates']['item'])) { + foreach($a_global['certificates']['item'] as $certificate) { + echo "Checking if renewal is needed for: {$certificate['name']}\n"; + renew_certificate($certificate['name']); + } } } @@ -280,6 +281,9 @@ function & get_certificate($name) { echo "Renewing certificate"; $domainstosign = array(); foreach($certificate['a_domainlist']['item'] as $domain) { + if ($domain['status'] == 'disable') { + continue; + } $domainstosign[] = $domain['name']; } @@ -298,11 +302,16 @@ function & get_certificate($name) { $handler->path = ""; $le->callback = $handler; $le->signDomains($domainstosign); - - foreach($certificate['a_actionlist']['item'] as $action) { - if ($action['method'] == "shellcommand") { - echo "Running {$action['command']}\n"; - mwexec_bg($action['command']); + if (is_array($certificate['a_actionlist']['item'])) { + foreach($certificate['a_actionlist']['item'] as $action) { + if ($action['method'] == "shellcommand") { + echo "Running {$action['command']}\n"; + mwexec_bg($action['command']); + } + if ($action['method'] == "php_command") { + echo "Running php {$action['command']}\n"; + eval($action['command']); + } } } syslog(LOG_NOTICE, "Acme, certificate renewed: {$id}"); @@ -320,12 +329,14 @@ function & get_certificate($name) { } } if ($domain_info['method'] == 'webroot') { + echo "webroot\n"; $directory = $domain_info['webrootfolder']; if(!file_exists($directory) && !@mkdir($directory, 0755, true)) { throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); } $tokenPath = $directory . "/" . $token; file_put_contents($tokenPath, $payload); + echo "put token at: {$tokenPath}\n"; } if ($domain_info['method'] == 'ftpwebroot') { echo "FTP\n"; @@ -356,14 +367,16 @@ function & get_certificate($name) { $certificatename = "acme_cert_" . $this->certificateinfo['name']; $cert = lookup_cert_by_name($certificatename); if (!is_array($cert)) { + echo "\n getCertificatePSK creating new cert"; global $config; $a_cert =& $config['cert']; $cert = array(); $cert['refid'] = uniqid(); $cert['descr'] = $certificatename; $accountkey = generateAccountKey(); - $cert['prv'] = base64_encode($accountkey['prv']); + $cert['prv'] = base64_encode($accountkey); $a_cert[] = $cert; + echo "\n{$cert['prv']}"; } return base64_decode($cert['prv']); } diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc index 376a8e349203..3aae27e5df76 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc @@ -134,6 +134,7 @@ class HtmlList } elseif ($itemtype == "fixedtext") { $result .= $item['text']; } else { + $itemvalue = htmlspecialchars($itemvalue, ENT_QUOTES); $result .= ""; } } else { diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc index b32f2957e6e7..9faa75bd7192 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc @@ -100,7 +100,6 @@ class Lescript // ---------------------------- foreach($domains as $domain) { - // 1. getting available authentication options // ------------------------------------------- @@ -146,7 +145,7 @@ class Lescript $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); + $this->log("Token for $domain should be available at $uri"); // simple self check if($payload !== trim(@file_get_contents($uri))) { @@ -347,7 +346,7 @@ keyUsage = nonRepudiation, digitalSignature, keyEncipherment'); openssl_csr_export($csr, $csr); fclose($tmpConf); - file_put_contents($this->getDomainPath($domain)."/last.csr", $csr); + //file_put_contents($this->getDomainPath($domain)."/last.csr", $csr); preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); From d5ebdad78b0ddc5cd614ad1655b45f9f636f1fa2 Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Fri, 23 Dec 2016 00:28:48 +0100 Subject: [PATCH 07/14] acme, changed underlying implementation to Neilpang/acme.sh / updated license / added service restart action --- security/pfSense-pkg-acme/Makefile | 8 +- .../files/etc/inc/priv/acme.priv.inc | 43 +- .../files/usr/local/pkg/acme.xml | 55 +- .../files/usr/local/pkg/acme/acme.inc | 364 +- .../files/usr/local/pkg/acme/acme.sh | 4587 +++++++++++++++++ .../files/usr/local/pkg/acme/acme_command.sh | 64 + .../files/usr/local/pkg/acme/acme_gui.inc | 43 +- .../usr/local/pkg/acme/acme_htmllist.inc | 44 +- .../local/pkg/acme/acme_serverconnectors.inc | 56 +- .../files/usr/local/pkg/acme/acme_sh.inc | 142 + .../files/usr/local/pkg/acme/acme_utils.inc | 44 +- .../files/usr/local/pkg/acme/lescript.inc | 534 -- .../usr/local/www/acme/acme_accountkeys.php | 62 +- .../local/www/acme/acme_accountkeys_edit.php | 72 +- .../usr/local/www/acme/acme_certificates.php | 65 +- .../local/www/acme/acme_certificates_edit.php | 68 +- .../local/www/acme/acme_generalsettings.php | 52 +- security/pfSense-pkg-acme/pkg-plist | 5 +- 18 files changed, 5219 insertions(+), 1089 deletions(-) create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc delete mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/lescript.inc diff --git a/security/pfSense-pkg-acme/Makefile b/security/pfSense-pkg-acme/Makefile index af2fec98e3df..e42abcb93604 100644 --- a/security/pfSense-pkg-acme/Makefile +++ b/security/pfSense-pkg-acme/Makefile @@ -34,6 +34,12 @@ do-install: ${STAGEDIR}${PREFIX}/pkg ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme.inc \ ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} -m 0755 ${FILESDIR}${PREFIX}/pkg/acme/acme.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_sh.inc \ + ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} -m 0755 ${FILESDIR}${PREFIX}/pkg/acme/acme_command.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_gui.inc \ ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_htmllist.inc \ @@ -42,8 +48,6 @@ do-install: ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/acme_utils.inc \ ${STAGEDIR}${PREFIX}/pkg/acme - ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/lescript.inc \ - ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/pkg_acme_tabs.inc \ ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_accountkeys.php \ diff --git a/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc b/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc index 7a145b9023dd..2318d9da0fe1 100644 --- a/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc +++ b/security/pfSense-pkg-acme/files/etc/inc/priv/acme.priv.inc @@ -1,31 +1,24 @@ acme diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index 27fdb339e4ab..ed2ff8b7ce10 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -1,37 +1,40 @@ "Let's Encrypt Staging (f $a_acmeserver['letsencrypt-production'] = array('name' => "Let's Encrypt Production(Applies ratelimits to certificate requests)", 'url' => 'https://acme-v01.api.letsencrypt.org' ); +/*$a_acmeserver['dummy'] = array('name' => "dummy", + 'url' => 'https://example.org' +);*/ global $acme_domain_validation_method; $acme_domain_validation_method = array(); @@ -89,27 +95,45 @@ $acme_domain_validation_method['ftpwebroot'] = array(name => "FTP webroot", 'description' =>"Folder the acme challenge response is written to for default: /.well-known/acme-challenge/" ) )); -/*$acme_domain_validation_method['http-post'] = array(name => "http-post", +//TODO add more challenge validation types +/* +$acme_domain_validation_method['http-post'] = array(name => "http-post", 'fields' => array( 'url' => array('name'=>"url",'columnheader'=>"Url",'type'=>"textbox",'size'=>"50", 'description' =>"Url the challenge file is posted to, the webserver there must store and reply to the request when the acme servers perform the request for the file from /.well-known/acme-challenge/" ) - ));*/ + )); +$acme_domain_validation_method['dns'] = array(name => "dns", + 'fields' => array( + 'url' => array('name'=>"url",'columnheader'=>"Url",'type'=>"textbox",'size'=>"50", + 'description' =>"Verify domain by adding a txt dns record" + ) + )); +*/ +//TODO add more 'actions' $acme_newcertificateactions = array(); $acme_newcertificateactions['shellcommand'] = array(name => "shell command"); +$acme_newcertificateactions['servicerestart'] = array(name => "restart service"); $acme_newcertificateactions['php_command'] = array(name => "php command script"); // #end +$a_keylength = array(); +$a_keylength['2048'] = array(name => "2048"); +$a_keylength['3072'] = array(name => "3072"); +$a_keylength['4096'] = array(name => "4096"); +$a_keylength['ec-256'] = array(name => "ec-256", ecc => true); +$a_keylength['ec-384'] = array(name => "ec-384", ecc => true); + function set_cronjob() { global $config; $a_global = &$config['installedpackages']['acme']; if (isset($a_global['enable'])) { - install_cron_job("/etc/rc.acme_renew.sh", true, "16", "3"); + install_cron_job('/usr/local/pkg/acme/acme_command.sh "renewall"', true, "16", "3"); } else { - install_cron_job("/etc/rc.acme_renew.sh", false); + install_cron_job('/usr/local/pkg/acme/acme_command.sh "renewall"', false); } } @@ -117,9 +141,6 @@ function acme_custom_php_deinstall_command() { global $static_output; $static_output .= "Acme, running acme_custom_php_deinstall_command()\n"; update_output_window($static_output); - $static_output .= "Acme, deleting renew_renew.sh\n"; - update_output_window($static_output); - unlink_if_exists("/etc/rc.acme_renew.sh"); $static_output .= "Acme, uninstalling cron job\n"; update_output_window($static_output); install_cron_job("/etc/rc.acme_renew.sh", false); @@ -131,34 +152,9 @@ function acme_custom_php_install_command() { global $static_output; $static_output .= "Acme, running acme_custom_php_install_command()\n"; update_output_window($static_output); - - $acme_renew = << - -EOD; - // removing the \r prevents the "No input file specified." error.. - $acme_renew = str_replace("\r\n","\n", $acme_renew); - $fd = fopen("/etc/rc.acme_renew.sh", "w"); - fwrite($fd, $acme_renew); - fclose($fd); - chmod("/etc/rc.acme_renew.sh", 0755); - + $static_output .= "Acme, installing cron job if enabled\n"; + update_output_window($static_output); set_cronjob(); - $static_output .= "Acme, running acme_custom_php_install_command() DONE\n"; update_output_window($static_output); } @@ -213,39 +209,21 @@ function & get_certificate($name) { return null; } - function createAcmeAccountKey() { - $certificatename = "acme_account_key"; - $cert = lookup_cert_by_name($certificatename); - if (!is_array($cert)) { - global $config; - $a_cert =& $config['cert']; - $cert = array(); - $cert['refid'] = uniqid(); - $cert['descr'] = $certificatename; - $accountkey = generateAccountKey(); - cert_import($cert, $accountkey['crt'], $accountkey['prv']); - $a_cert[] = $cert; - $changedesc = "Services: Acme"; - $changedesc .= " created acme account key"; - write_config($changedesc); - } + function generateAccountKey($name, $ca) { + $acmesh = new acme_sh($name, $ca); + return $acmesh->generateAccountKey(); } - function generateAccountKey() { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_RSA, - "private_key_bits" => 4096, - )); - if(!openssl_pkey_export($res, $privateKey)) { - throw new \RuntimeException("Key export failed!"); - } - return $privateKey; + + function generateDomainKey($name, $ca, $domain, $keylength) { + $acmesh = new acme_sh($name, $ca); + return $acmesh->generateDomainKey($domain, $keylength); } - - function getAcmeClient($ca) { - $logger = new Logger(); - $le = \Analogic\ACME\Lescript::createWithCustomEvents($ca, $logger); - return $le; + + function registerAcmeAccountKey($name, $ca, $key) { + $acmesh = new acme_sh($name, $ca); + return $acmesh->registeraccount($key); } + function renew_all_certificates() { global $config; $a_global = &$config['installedpackages']['acme']; @@ -257,6 +235,45 @@ function & get_certificate($name) { } } + function getCertificatePSK($ca, $certificate, $domain) { + $certificatename = $certificate['name']; + $cert = lookup_cert_by_name($certificatename); + if (!is_array($cert)) { + echo "\n getCertificatePSK creating new cert"; + global $config; + $a_cert =& $config['cert']; + $cert = array(); + $cert['refid'] = uniqid(); + $cert['descr'] = $certificatename; + $accountkey = generateDomainKey($certificatename, $ca, $domain, $certificate['keylength']); + $cert['prv'] = base64_encode($accountkey); + $a_cert[] = $cert; + echo "\n{$cert['prv']}"; + $desc = "Acme: Add new certificate & key."; + write_config($desc); + } + return base64_decode($cert['prv']); + } + + function storeCertificateCer($certificatename, $keyfile, $cerfile) { + global $config; + $key = file_get_contents($keyfile); + $crt = file_get_contents($cerfile); + if (is_array($config['cert'])) { + foreach ($config['cert'] as &$cert) { + if ($cert['descr'] == $certificatename) { + syslog(LOG_NOTICE, "Acme, storing new certificate: {$certificatename}"); + echo "update cert!"; + $cert['key'] = base64_encode($key); + $cert['crt'] = base64_encode($crt); + return true; + } + } + } + //TODO: store chain to.?. + return false; + } + function renew_certificate($id, $force = false) { $certificate = & get_certificate($id); if (!$force) { @@ -265,14 +282,17 @@ function & get_certificate($name) { return; } + $renewafterdays = is_numericint($certificate['renewafter']) ? $certificate['renewafter'] : 60; $timetorenew = false; $now = new \DateTime(); $lastrenewal = new \DateTime(); $lastrenewal->setTimestamp($certificate['lastrenewal']); - $nextrenewal = $lastrenewal->add(new \DateInterval('P'.$certificate['renewafter'].'D')); + $nextrenewal = $lastrenewal->add(new \DateInterval('P'.$renewafterdays.'D')); if ($now >= $nextrenewal) { echo "## Its time to renew ##\n"; $timetorenew = true; + } else { + echo "Renewal number of days not yet reached."; } } @@ -290,136 +310,62 @@ function & get_certificate($name) { echo "account: {$certificate['acmeaccount']} \n"; $account = get_accountkey($certificate['acmeaccount']); $acmeserver = $account['acmeserver']; - $key = $account['accountkey']; + $accountkey = $account['accountkey']; echo "server: $acmeserver \n"; + global $a_acmeserver; $url = $a_acmeserver[$acmeserver]['url']; - $le = getAcmeClient($url); - $le->setPrivateKey(base64_decode($key)); - - $handler = new acme_handler(); - $handler->certificateinfo = & $certificate; - $handler->path = ""; - $le->callback = $handler; - $le->signDomains($domainstosign); - if (is_array($certificate['a_actionlist']['item'])) { - foreach($certificate['a_actionlist']['item'] as $action) { - if ($action['method'] == "shellcommand") { - echo "Running {$action['command']}\n"; - mwexec_bg($action['command']); - } - if ($action['method'] == "php_command") { - echo "Running php {$action['command']}\n"; - eval($action['command']); - } - } - } - syslog(LOG_NOTICE, "Acme, certificate renewed: {$id}"); + $certificatepsk = getCertificatePSK($url, $certificate, $domainstosign[0]); + $acmesh = new acme_sh($certificate['name'], $url); + $acmesh->signCertificate($accountkey, $certificatepsk, $domainstosign); } } - class acme_handler { - public $path = ""; - public $certificateinfo = null; - private $ftp = null; - function chalenge_response_put($domain, $token, $payload){ - echo "\nchalenge_response_put\n"; - foreach($this->certificateinfo['a_domainlist']['item'] as $domainitem) { - if($domainitem['name'] == $domain){ - $domain_info = $domainitem; - } - } - if ($domain_info['method'] == 'webroot') { - echo "webroot\n"; - $directory = $domain_info['webrootfolder']; - if(!file_exists($directory) && !@mkdir($directory, 0755, true)) { - throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); - } - $tokenPath = $directory . "/" . $token; - file_put_contents($tokenPath, $payload); - echo "put token at: {$tokenPath}\n"; - } - if ($domain_info['method'] == 'ftpwebroot') { - echo "FTP\n"; - $this->ftp = new FTPConnection($domain_info['ftpwebrootftpserver']); - $this->ftp->login($domain_info['ftpwebrootusername'], $domain_info['ftpwebrootpassword']); - $directory = $domain_info['ftpwebrootfolder']; - $tokenPath = $directory . "/" . $token; - $this->ftp->mkdir($directory); - $this->ftp->uploadData($payload, $tokenPath); + + function challenge_response_put($certificatename, $domain, $token, $payload) { + $acmecert = get_certificate($certificatename); + + echo "\nchalenge_response_put $certificatename, $domain\n"; + foreach($acmecert['a_domainlist']['item'] as $domainitem) { + if($domainitem['name'] == $domain){ + $domain_info = $domainitem; + echo "FOUND domainitem"; } } - function chalenge_response_cleanup($domain, $token) { - foreach($this->certificateinfo['a_domainlist']['item'] as $domainitem) { - if($domainitem['name'] == $domain){ - $domain_info = $domainitem; - } - } - if ($domain_info['method'] == 'webroot') { - $tokenfile = $domain_info['webrootfolder'] . "/" . $token; - @unlink($tokenfile); - } - if ($domain_info['method'] == 'ftpwebroot') { - $tokenfile = $domain_info['ftpwebrootfolder'] . "/" . $token; - $this->ftp->deleteFile($tokenfile); + if ($domain_info['method'] == 'webroot') { + echo "webroot\n"; + $directory = $domain_info['webrootfolder']; + if(!file_exists($directory) && !@mkdir($directory, 0755, true)) { + throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); } + $tokenPath = $directory . "/" . $token; + file_put_contents($tokenPath, $payload); + echo "put token at: {$tokenPath}\n"; } - function getCertificatePSK() { - $certificatename = "acme_cert_" . $this->certificateinfo['name']; - $cert = lookup_cert_by_name($certificatename); - if (!is_array($cert)) { - echo "\n getCertificatePSK creating new cert"; - global $config; - $a_cert =& $config['cert']; - $cert = array(); - $cert['refid'] = uniqid(); - $cert['descr'] = $certificatename; - $accountkey = generateAccountKey(); - $cert['prv'] = base64_encode($accountkey); - $a_cert[] = $cert; - echo "\n{$cert['prv']}"; - } - return base64_decode($cert['prv']); + if ($domain_info['method'] == 'ftpwebroot') { + echo "FTP\n"; + $this->ftp = new FTPConnection($domain_info['ftpwebrootftpserver']); + $this->ftp->login($domain_info['ftpwebrootusername'], $domain_info['ftpwebrootpassword']); + $directory = $domain_info['ftpwebrootfolder']; + $tokenPath = $directory . "/" . $token; + $this->ftp->mkdir($directory); + $this->ftp->uploadData($payload, $tokenPath); } - function storeCertificate($certificates) { - $certificatename = "acme_cert_" . $this->certificateinfo['name']; - global $config; - if (is_array($config['cert'])) { - foreach ($config['cert'] as &$cert) { - if ($cert['descr'] == $certificatename) { - //TODO add validation that the new cert 'fits' on the private key.. - $cert['crt'] = base64_encode(array_shift($certificates)); - - $id = get_certificate_id($this->certificateinfo['name']); - $a_certificates = &$config['installedpackages']['acme']['certificates']['item']; - $a_certificates[$id]['lastrenewal'] = time(); - } - } - } - //TODO: store chain to.?. - //$cert = lookup_cert_by_name($certificatename); + } - //if (is_array($cert)) { - //$cert['crt'] = array_shift($certificates); - //echo "NEW CRT: ".base64_decode($cert['crt']); - //} - $changedesc = "Services: Acme"; - $changedesc .= "Storing signed certificate"; - write_config($changedesc); + function chalenge_response_cleanup($certificatename, $domain, $token) { + $acmecert = get_certificate($certificatename); + foreach($acmecert['a_domainlist']['item'] as $domainitem) { + if($domainitem['name'] == $domain){ + $domain_info = $domainitem; + } } - } - - function registerAcmeAccountKey($ca, $key) { - $logger = new Logger(); - $le = \Analogic\ACME\Lescript::createWithCustomEvents($ca, $logger); - $le->setPrivateKey($key); - $result = $le->postNewReg(); - return $result; - } - - class Logger { - function __call($name, $arguments) { - echo date('Y-m-d H:i:s')." [$name] ${arguments[0]}\n"; + if ($domain_info['method'] == 'webroot') { + $tokenfile = $domain_info['webrootfolder'] . "/" . $token; + @unlink($tokenfile); + } + if ($domain_info['method'] == 'ftpwebroot') { + $tokenfile = $domain_info['ftpwebrootfolder'] . "/" . $token; + $this->ftp->deleteFile($tokenfile); } } - } \ No newline at end of file diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.sh new file mode 100644 index 000000000000..9615582565e5 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.sh @@ -0,0 +1,4587 @@ +#!/usr/bin/env sh + +VER=2.6.5 + +PROJECT_NAME="acme.sh" + +PROJECT_ENTRY="acme.sh" + +PROJECT="https://github.com/Neilpang/$PROJECT_NAME" + +DEFAULT_INSTALL_HOME="$HOME/.$PROJECT_NAME" +_SCRIPT_="$0" + +_SUB_FOLDERS="dnsapi deploy" + +DEFAULT_CA="https://acme-v01.api.letsencrypt.org" +DEFAULT_AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" + +DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" +DEFAULT_ACCOUNT_EMAIL="" + +DEFAULT_ACCOUNT_KEY_LENGTH=2048 +DEFAULT_DOMAIN_KEY_LENGTH=2048 + +DEFAULT_OPENSSL_BIN="openssl" + +STAGE_CA="https://acme-staging.api.letsencrypt.org" + +VTYPE_HTTP="http-01" +VTYPE_DNS="dns-01" +VTYPE_TLS="tls-sni-01" +#VTYPE_TLS2="tls-sni-02" + +LOCAL_ANY_ADDRESS="0.0.0.0" + +MAX_RENEW=60 + +DEFAULT_DNS_SLEEP=120 + +NO_VALUE="no" + +W_TLS="tls" + +STATE_VERIFIED="verified_ok" + +BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----" +END_CSR="-----END CERTIFICATE REQUEST-----" + +BEGIN_CERT="-----BEGIN CERTIFICATE-----" +END_CERT="-----END CERTIFICATE-----" + +RENEW_SKIP=2 + +ECC_SEP="_" +ECC_SUFFIX="${ECC_SEP}ecc" + +LOG_LEVEL_1=1 +LOG_LEVEL_2=2 +LOG_LEVEL_3=3 +DEFAULT_LOG_LEVEL="$LOG_LEVEL_1" + +_DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh" + +__INTERACTIVE="" +if [ -t 1 ]; then + __INTERACTIVE="1" +fi + +__green() { + if [ "$__INTERACTIVE" ]; then + printf '\033[1;31;32m' + fi + printf -- "$1" + if [ "$__INTERACTIVE" ]; then + printf '\033[0m' + fi +} + +__red() { + if [ "$__INTERACTIVE" ]; then + printf '\033[1;31;40m' + fi + printf -- "$1" + if [ "$__INTERACTIVE" ]; then + printf '\033[0m' + fi +} + +_printargs() { + if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then + printf -- "%s" "[$(date)] " + fi + if [ -z "$2" ]; then + printf -- "%s" "$1" + else + printf -- "%s" "$1='$2'" + fi + printf "\n" +} + +_dlg_versions() { + echo "Diagnosis versions: " + echo "openssl:$OPENSSL_BIN" + if _exists "$OPENSSL_BIN"; then + $OPENSSL_BIN version 2>&1 + else + echo "$OPENSSL_BIN doesn't exists." + fi + + echo "apache:" + if [ "$_APACHECTL" ] && _exists "$_APACHECTL"; then + _APACHECTL -V 2>&1 + else + echo "apache doesn't exists." + fi + + echo "nc:" + if _exists "nc"; then + nc -h 2>&1 + else + _debug "nc doesn't exists." + fi +} + +_log() { + [ -z "$LOG_FILE" ] && return + _printargs "$@" >>"$LOG_FILE" +} + +_info() { + _log "$@" + _printargs "$@" +} + +_err() { + _log "$@" + if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then + printf -- "%s" "[$(date)] " >&2 + fi + if [ -z "$2" ]; then + __red "$1" >&2 + else + __red "$1='$2'" >&2 + fi + printf "\n" >&2 + return 1 +} + +_usage() { + __red "$@" >&2 + printf "\n" >&2 +} + +_debug() { + if [ -z "$LOG_LEVEL" ] || [ "$LOG_LEVEL" -ge "$LOG_LEVEL_1" ]; then + _log "$@" + fi + if [ -z "$DEBUG" ]; then + return + fi + _printargs "$@" >&2 +} + +_debug2() { + if [ "$LOG_LEVEL" ] && [ "$LOG_LEVEL" -ge "$LOG_LEVEL_2" ]; then + _log "$@" + fi + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _debug "$@" + fi +} + +_debug3() { + if [ "$LOG_LEVEL" ] && [ "$LOG_LEVEL" -ge "$LOG_LEVEL_3" ]; then + _log "$@" + fi + if [ "$DEBUG" ] && [ "$DEBUG" -ge "3" ]; then + _debug "$@" + fi +} + +_startswith() { + _str="$1" + _sub="$2" + echo "$_str" | grep "^$_sub" >/dev/null 2>&1 +} + +_endswith() { + _str="$1" + _sub="$2" + echo "$_str" | grep -- "$_sub\$" >/dev/null 2>&1 +} + +_contains() { + _str="$1" + _sub="$2" + echo "$_str" | grep -- "$_sub" >/dev/null 2>&1 +} + +_hasfield() { + _str="$1" + _field="$2" + _sep="$3" + if [ -z "$_field" ]; then + _usage "Usage: str field [sep]" + return 1 + fi + + if [ -z "$_sep" ]; then + _sep="," + fi + + for f in $(echo "$_str" | tr ',' ' '); do + if [ "$f" = "$_field" ]; then + _debug2 "'$_str' contains '$_field'" + return 0 #contains ok + fi + done + _debug2 "'$_str' does not contain '$_field'" + return 1 #not contains +} + +_getfield() { + _str="$1" + _findex="$2" + _sep="$3" + + if [ -z "$_findex" ]; then + _usage "Usage: str field [sep]" + return 1 + fi + + if [ -z "$_sep" ]; then + _sep="," + fi + + _ffi="$_findex" + while [ "$_ffi" -gt "0" ]; do + _fv="$(echo "$_str" | cut -d "$_sep" -f "$_ffi")" + if [ "$_fv" ]; then + printf -- "%s" "$_fv" + return 0 + fi + _ffi="$(_math "$_ffi" - 1)" + done + + printf -- "%s" "$_str" + +} + +_exists() { + cmd="$1" + if [ -z "$cmd" ]; then + _usage "Usage: _exists cmd" + return 1 + fi + + if eval type type >/dev/null 2>&1; then + eval type "$cmd" >/dev/null 2>&1 + elif command >/dev/null 2>&1; then + command -v "$cmd" >/dev/null 2>&1 + else + which "$cmd" >/dev/null 2>&1 + fi + ret="$?" + _debug3 "$cmd exists=$ret" + return $ret +} + +#a + b +_math() { + _m_opts="$@" + printf "%s" "$(($_m_opts))" +} + +_h_char_2_dec() { + _ch=$1 + case "${_ch}" in + a | A) + printf "10" + ;; + b | B) + printf "11" + ;; + c | C) + printf "12" + ;; + d | D) + printf "13" + ;; + e | E) + printf "14" + ;; + f | F) + printf "15" + ;; + *) + printf "%s" "$_ch" + ;; + esac + +} + +_URGLY_PRINTF="" +if [ "$(printf '\x41')" != 'A' ]; then + _URGLY_PRINTF=1 +fi + +_h2b() { + hex=$(cat) + i=1 + j=2 + + _debug3 _URGLY_PRINTF "$_URGLY_PRINTF" + while true; do + if [ -z "$_URGLY_PRINTF" ]; then + h="$(printf "%s" "$hex" | cut -c $i-$j)" + if [ -z "$h" ]; then + break + fi + printf "\x$h%s" + else + ic="$(printf "%s" "$hex" | cut -c $i)" + jc="$(printf "%s" "$hex" | cut -c $j)" + if [ -z "$ic$jc" ]; then + break + fi + ic="$(_h_char_2_dec "$ic")" + jc="$(_h_char_2_dec "$jc")" + printf '\'"$(printf "%o" "$(_math "$ic" \* 16 + $jc)")""%s" + fi + + i="$(_math "$i" + 2)" + j="$(_math "$j" + 2)" + + done +} + +#hex string +_hex() { + _str="$1" + _str_len=${#_str} + _h_i=1 + while [ "$_h_i" -le "$_str_len" ]; do + _str_c="$(printf "%s" "$_str" | cut -c "$_h_i")" + printf "%02x" "'$_str_c" + _h_i="$(_math "$_h_i" + 1)" + done +} + +#options file +_sed_i() { + options="$1" + filename="$2" + if [ -z "$filename" ]; then + _usage "Usage:_sed_i options filename" + return 1 + fi + _debug2 options "$options" + if sed -h 2>&1 | grep "\-i\[SUFFIX]" >/dev/null 2>&1; then + _debug "Using sed -i" + sed -i "$options" "$filename" + else + _debug "No -i support in sed" + text="$(cat "$filename")" + echo "$text" | sed "$options" >"$filename" + fi +} + +_egrep_o() { + if ! egrep -o "$1" 2>/dev/null; then + sed -n 's/.*\('"$1"'\).*/\1/p' + fi +} + +#Usage: file startline endline +_getfile() { + filename="$1" + startline="$2" + endline="$3" + if [ -z "$endline" ]; then + _usage "Usage: file startline endline" + return 1 + fi + + i="$(grep -n -- "$startline" "$filename" | cut -d : -f 1)" + if [ -z "$i" ]; then + _err "Can not find start line: $startline" + return 1 + fi + i="$(_math "$i" + 1)" + _debug i "$i" + + j="$(grep -n -- "$endline" "$filename" | cut -d : -f 1)" + if [ -z "$j" ]; then + _err "Can not find end line: $endline" + return 1 + fi + j="$(_math "$j" - 1)" + _debug j "$j" + + sed -n "$i,${j}p" "$filename" + +} + +#Usage: multiline +_base64() { + if [ "$1" ]; then + $OPENSSL_BIN base64 -e + else + $OPENSSL_BIN base64 -e | tr -d '\r\n' + fi +} + +#Usage: multiline +_dbase64() { + if [ "$1" ]; then + $OPENSSL_BIN base64 -d -A + else + $OPENSSL_BIN base64 -d + fi +} + +#Usage: hashalg [outputhex] +#Output Base64-encoded digest +_digest() { + alg="$1" + if [ -z "$alg" ]; then + _usage "Usage: _digest hashalg" + return 1 + fi + + outputhex="$2" + + if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then + if [ "$outputhex" ]; then + $OPENSSL_BIN dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' + else + $OPENSSL_BIN dgst -"$alg" -binary | _base64 + fi + else + _err "$alg is not supported yet" + return 1 + fi + +} + +#Usage: hashalg secret_hex [outputhex] +#Output binary hmac +_hmac() { + alg="$1" + secret_hex="$2" + outputhex="$3" + + if [ -z "$secret_hex" ]; then + _usage "Usage: _hmac hashalg secret [outputhex]" + return 1 + fi + + if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then + if [ "$outputhex" ]; then + $OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" | cut -d = -f 2 | tr -d ' ' + else + $OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary + fi + else + _err "$alg is not supported yet" + return 1 + fi + +} + +#Usage: keyfile hashalg +#Output: Base64-encoded signature value +_sign() { + keyfile="$1" + alg="$2" + if [ -z "$alg" ]; then + _usage "Usage: _sign keyfile hashalg" + return 1 + fi + + _sign_openssl="$OPENSSL_BIN dgst -sign $keyfile " + if [ "$alg" = "sha256" ]; then + _sign_openssl="$_sign_openssl -$alg" + else + _err "$alg is not supported yet" + return 1 + fi + + if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + $_sign_openssl | _base64 + elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + if ! _signedECText="$($_sign_openssl | $OPENSSL_BIN asn1parse -inform DER)"; then + _err "Sign failed: $_sign_openssl" + _err "Key file: $keyfile" + _err "Key content:$(wc -l <"$keyfile") lises" + return 1 + fi + _debug3 "_signedECText" "$_signedECText" + _ec_r="$(echo "$_signedECText" | _head_n 2 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" + _debug3 "_ec_r" "$_ec_r" + _ec_s="$(echo "$_signedECText" | _head_n 3 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" + _debug3 "_ec_s" "$_ec_s" + printf "%s" "$_ec_r$_ec_s" | _h2b | _base64 + else + _err "Unknown key file format." + return 1 + fi + +} + +#keylength +_isEccKey() { + _length="$1" + + if [ -z "$_length" ]; then + return 1 + fi + + [ "$_length" != "1024" ] \ + && [ "$_length" != "2048" ] \ + && [ "$_length" != "3072" ] \ + && [ "$_length" != "4096" ] \ + && [ "$_length" != "8192" ] +} + +# _createkey 2048|ec-256 file +_createkey() { + length="$1" + f="$2" + _debug2 "_createkey for file:$f" + eccname="$length" + if _startswith "$length" "ec-"; then + length=$(printf "%s" "$length" | cut -d '-' -f 2-100) + + if [ "$length" = "256" ]; then + eccname="prime256v1" + fi + if [ "$length" = "384" ]; then + eccname="secp384r1" + fi + if [ "$length" = "521" ]; then + eccname="secp521r1" + fi + + fi + + if [ -z "$length" ]; then + length=2048 + fi + + _debug "Use length $length" + + if _isEccKey "$length"; then + _debug "Using ec name: $eccname" + $OPENSSL_BIN ecparam -name "$eccname" -genkey 2>/dev/null >"$f" + else + _debug "Using RSA: $length" + $OPENSSL_BIN genrsa "$length" 2>/dev/null >"$f" + fi + + if [ "$?" != "0" ]; then + _err "Create key error." + return 1 + fi +} + +#domain +_is_idn() { + _is_idn_d="$1" + _debug2 _is_idn_d "$_is_idn_d" + _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '[0-9]' | tr -d '[a-z]' | tr -d '[A-Z]' | tr -d '.,-') + _debug2 _idn_temp "$_idn_temp" + [ "$_idn_temp" ] +} + +#aa.com +#aa.com,bb.com,cc.com +_idn() { + __idn_d="$1" + if ! _is_idn "$__idn_d"; then + printf "%s" "$__idn_d" + return 0 + fi + + if _exists idn; then + if _contains "$__idn_d" ','; then + _i_first="1" + for f in $(echo "$__idn_d" | tr ',' ' '); do + [ -z "$f" ] && continue + if [ -z "$_i_first" ]; then + printf "%s" "," + else + _i_first="" + fi + idn --quiet "$f" | tr -d "\r\n" + done + else + idn "$__idn_d" | tr -d "\r\n" + fi + else + _err "Please install idn to process IDN names." + fi +} + +#_createcsr cn san_list keyfile csrfile conf +_createcsr() { + _debug _createcsr + domain="$1" + domainlist="$2" + csrkey="$3" + csr="$4" + csrconf="$5" + _debug2 domain "$domain" + _debug2 domainlist "$domainlist" + _debug2 csrkey "$csrkey" + _debug2 csr "$csr" + _debug2 csrconf "$csrconf" + + printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\n\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment" >"$csrconf" + + if [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then + #single domain + _info "Single domain" "$domain" + else + domainlist="$(_idn "$domainlist")" + _debug2 domainlist "$domainlist" + if _contains "$domainlist" ","; then + alt="DNS:$(echo "$domainlist" | sed "s/,/,DNS:/g")" + else + alt="DNS:$domainlist" + fi + #multi + _info "Multi domain" "$alt" + printf -- "\nsubjectAltName=$alt" >>"$csrconf" + fi + if [ "$Le_OCSP_Stable" ]; then + _savedomainconf Le_OCSP_Stable "$Le_OCSP_Stable" + printf -- "\nbasicConstraints = CA:FALSE\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >>"$csrconf" + fi + + _csr_cn="$(_idn "$domain")" + _debug2 _csr_cn "$_csr_cn" + $OPENSSL_BIN req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" +} + +#_signcsr key csr conf cert +_signcsr() { + key="$1" + csr="$2" + conf="$3" + cert="$4" + _debug "_signcsr" + + _msg="$($OPENSSL_BIN x509 -req -days 365 -in "$csr" -signkey "$key" -extensions v3_req -extfile "$conf" -out "$cert" 2>&1)" + _ret="$?" + _debug "$_msg" + return $_ret +} + +#_csrfile +_readSubjectFromCSR() { + _csrfile="$1" + if [ -z "$_csrfile" ]; then + _usage "_readSubjectFromCSR mycsr.csr" + return 1 + fi + $OPENSSL_BIN req -noout -in "$_csrfile" -subject | _egrep_o "CN=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d '\n' +} + +#_csrfile +#echo comma separated domain list +_readSubjectAltNamesFromCSR() { + _csrfile="$1" + if [ -z "$_csrfile" ]; then + _usage "_readSubjectAltNamesFromCSR mycsr.csr" + return 1 + fi + + _csrsubj="$(_readSubjectFromCSR "$_csrfile")" + _debug _csrsubj "$_csrsubj" + + _dnsAltnames="$($OPENSSL_BIN req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" + _debug _dnsAltnames "$_dnsAltnames" + + if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then + _debug "AltNames contains subject" + _dnsAltnames="$(printf "%s" "$_dnsAltnames," | sed "s/DNS:$_csrsubj,//g")" + else + _debug "AltNames doesn't contain subject" + fi + + printf "%s" "$_dnsAltnames" | sed "s/DNS://g" +} + +#_csrfile +_readKeyLengthFromCSR() { + _csrfile="$1" + if [ -z "$_csrfile" ]; then + _usage "_readKeyLengthFromCSR mycsr.csr" + return 1 + fi + + _outcsr="$($OPENSSL_BIN req -noout -text -in "$_csrfile")" + if _contains "$_outcsr" "Public Key Algorithm: id-ecPublicKey"; then + _debug "ECC CSR" + echo "$_outcsr" | _egrep_o "^ *ASN1 OID:.*" | cut -d ':' -f 2 | tr -d ' ' + else + _debug "RSA CSR" + echo "$_outcsr" | _egrep_o "^ *Public-Key:.*" | cut -d '(' -f 2 | cut -d ' ' -f 1 + fi +} + +_ss() { + _port="$1" + + if _exists "ss"; then + _debug "Using: ss" + ss -ntpl | grep ":$_port " + return 0 + fi + + if _exists "netstat"; then + _debug "Using: netstat" + if netstat -h 2>&1 | grep "\-p proto" >/dev/null; then + #for windows version netstat tool + netstat -an -p tcp | grep "LISTENING" | grep ":$_port " + else + if netstat -help 2>&1 | grep "\-p protocol" >/dev/null; then + netstat -an -p tcp | grep LISTEN | grep ":$_port " + elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then + #for solaris + netstat -an -P tcp | grep "\.$_port " | grep "LISTEN" + else + netstat -ntpl | grep ":$_port " + fi + fi + return 0 + fi + + return 1 +} + +#domain [password] [isEcc] +toPkcs() { + domain="$1" + pfxPassword="$2" + if [ -z "$domain" ]; then + _usage "Usage: $PROJECT_ENTRY --toPkcs -d domain [--password pfx-password]" + return 1 + fi + + _isEcc="$3" + + _initpath "$domain" "$_isEcc" + + if [ "$pfxPassword" ]; then + $OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" -password "pass:$pfxPassword" + else + $OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" + fi + + if [ "$?" = "0" ]; then + _info "Success, Pfx is exported to: $CERT_PFX_PATH" + fi + +} + +#[2048] +createAccountKey() { + _info "Creating account key" + if [ -z "$1" ]; then + _usage "Usage: $PROJECT_ENTRY --createAccountKey --accountkeylength 2048" + return + fi + + length=$1 + _create_account_key "$length" + +} + +_create_account_key() { + + length=$1 + + if [ -z "$length" ] || [ "$length" = "$NO_VALUE" ]; then + _debug "Use default length $DEFAULT_ACCOUNT_KEY_LENGTH" + length="$DEFAULT_ACCOUNT_KEY_LENGTH" + fi + + _debug length "$length" + _initpath + + mkdir -p "$CA_DIR" + if [ -f "$ACCOUNT_KEY_PATH" ]; then + _info "Account key exists, skip" + return + else + #generate account key + _createkey "$length" "$ACCOUNT_KEY_PATH" + fi + +} + +#domain [length] +createDomainKey() { + _info "Creating domain key" + if [ -z "$1" ]; then + _usage "Usage: $PROJECT_ENTRY --createDomainKey -d domain.com [ --keylength 2048 ]" + return + fi + + domain=$1 + length=$2 + + if [ -z "$length" ]; then + _debug "Use DEFAULT_DOMAIN_KEY_LENGTH=$DEFAULT_DOMAIN_KEY_LENGTH" + length="$DEFAULT_DOMAIN_KEY_LENGTH" + fi + + _initpath "$domain" "$length" + + if [ ! -f "$CERT_KEY_PATH" ] || ([ "$FORCE" ] && ! [ "$IS_RENEW" ]); then + _createkey "$length" "$CERT_KEY_PATH" + else + if [ "$IS_RENEW" ]; then + _info "Domain key exists, skip" + return 0 + else + _err "Domain key exists, do you want to overwrite the key?" + _err "Add '--force', and try again." + return 1 + fi + fi + +} + +# domain domainlist isEcc +createCSR() { + _info "Creating csr" + if [ -z "$1" ]; then + _usage "Usage: $PROJECT_ENTRY --createCSR -d domain1.com [-d domain2.com -d domain3.com ... ]" + return + fi + + domain="$1" + domainlist="$2" + _isEcc="$3" + + _initpath "$domain" "$_isEcc" + + if [ -f "$CSR_PATH" ] && [ "$IS_RENEW" ] && [ -z "$FORCE" ]; then + _info "CSR exists, skip" + return + fi + + if [ ! -f "$CERT_KEY_PATH" ]; then + _err "The key file is not found: $CERT_KEY_PATH" + _err "Please create the key file first." + return 1 + fi + _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" + +} + +_urlencode() { + tr '/+' '_-' | tr -d '= ' +} + +_time2str() { + #BSD + if date -u -d@"$1" 2>/dev/null; then + return + fi + + #Linux + if date -u -r "$1" 2>/dev/null; then + return + fi + + #Soaris + if _exists adb; then + _t_s_a=$(echo "0t${1}=Y" | adb) + echo "$_t_s_a" + fi + +} + +_normalizeJson() { + sed "s/\" *: *\([\"{\[]\)/\":\1/g" | sed "s/^ *\([^ ]\)/\1/" | tr -d "\r\n" +} + +_stat() { + #Linux + if stat -c '%U:%G' "$1" 2>/dev/null; then + return + fi + + #BSD + if stat -f '%Su:%Sg' "$1" 2>/dev/null; then + return + fi + + return 1 #error, 'stat' not found +} + +#keyfile +_calcjwk() { + keyfile="$1" + if [ -z "$keyfile" ]; then + _usage "Usage: _calcjwk keyfile" + return 1 + fi + + if [ "$JWK_HEADER" ] && [ "$__CACHED_JWK_KEY_FILE" = "$keyfile" ]; then + _debug2 "Use cached jwk for file: $__CACHED_JWK_KEY_FILE" + return 0 + fi + + if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + _debug "RSA key" + pub_exp=$($OPENSSL_BIN rsa -in "$keyfile" -noout -text | grep "^publicExponent:" | cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1) + if [ "${#pub_exp}" = "5" ]; then + pub_exp=0$pub_exp + fi + _debug3 pub_exp "$pub_exp" + + e=$(echo "$pub_exp" | _h2b | _base64) + _debug3 e "$e" + + modulus=$($OPENSSL_BIN rsa -in "$keyfile" -modulus -noout | cut -d '=' -f 2) + _debug3 modulus "$modulus" + n="$(printf "%s" "$modulus" | _h2b | _base64 | _urlencode)" + jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}' + _debug3 jwk "$jwk" + + JWK_HEADER='{"alg": "RS256", "jwk": '$jwk'}' + JWK_HEADERPLACE_PART1='{"nonce": "' + JWK_HEADERPLACE_PART2='", "alg": "RS256", "jwk": '$jwk'}' + elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + _debug "EC key" + crv="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" | cut -d ":" -f 2 | tr -d " \r\n")" + _debug3 crv "$crv" + + if [ -z "$crv" ]; then + _debug "Let's try ASN1 OID" + crv_oid="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^ASN1 OID:" | cut -d ":" -f 2 | tr -d " \r\n")" + _debug3 crv_oid "$crv_oid" + case "${crv_oid}" in + "prime256v1") + crv="P-256" + ;; + "secp384r1") + crv="P-384" + ;; + "secp521r1") + crv="P-521" + ;; + *) + _err "ECC oid : $crv_oid" + return 1 + ;; + esac + _debug3 crv "$crv" + fi + + pubi="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n pub: | cut -d : -f 1)" + pubi=$(_math "$pubi" + 1) + _debug3 pubi "$pubi" + + pubj="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n "ASN1 OID:" | cut -d : -f 1)" + pubj=$(_math "$pubj" - 1) + _debug3 pubj "$pubj" + + pubtext="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | sed -n "$pubi,${pubj}p" | tr -d " \n\r")" + _debug3 pubtext "$pubtext" + + xlen="$(printf "%s" "$pubtext" | tr -d ':' | wc -c)" + xlen=$(_math "$xlen" / 4) + _debug3 xlen "$xlen" + + xend=$(_math "$xlen" + 1) + x="$(printf "%s" "$pubtext" | cut -d : -f 2-"$xend")" + _debug3 x "$x" + + x64="$(printf "%s" "$x" | tr -d : | _h2b | _base64 | _urlencode)" + _debug3 x64 "$x64" + + xend=$(_math "$xend" + 1) + y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-10000)" + _debug3 y "$y" + + y64="$(printf "%s" "$y" | tr -d : | _h2b | _base64 | _urlencode)" + _debug3 y64 "$y64" + + jwk='{"crv": "'$crv'", "kty": "EC", "x": "'$x64'", "y": "'$y64'"}' + _debug3 jwk "$jwk" + + JWK_HEADER='{"alg": "ES256", "jwk": '$jwk'}' + JWK_HEADERPLACE_PART1='{"nonce": "' + JWK_HEADERPLACE_PART2='", "alg": "ES256", "jwk": '$jwk'}' + else + _err "Only RSA or EC key is supported." + return 1 + fi + + _debug3 JWK_HEADER "$JWK_HEADER" + __CACHED_JWK_KEY_FILE="$keyfile" +} + +_time() { + date -u "+%s" +} + +_mktemp() { + if _exists mktemp; then + if mktemp 2>/dev/null; then + return 0 + elif _contains "$(mktemp 2>&1)" "-t prefix" && mktemp -t "$PROJECT_NAME" 2>/dev/null; then + #for Mac osx + return 0 + fi + fi + if [ -d "/tmp" ]; then + echo "/tmp/${PROJECT_NAME}wefADf24sf.$(_time).tmp" + return 0 + elif [ "$LE_TEMP_DIR" ] && mkdir -p "$LE_TEMP_DIR"; then + echo "/$LE_TEMP_DIR/wefADf24sf.$(_time).tmp" + return 0 + fi + _err "Can not create temp file." +} + +_inithttp() { + + if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then + HTTP_HEADER="$(_mktemp)" + _debug2 HTTP_HEADER "$HTTP_HEADER" + fi + + if [ "$__HTTP_INITIALIZED" ]; then + if [ "$_ACME_CURL$_ACME_WGET" ]; then + _debug2 "Http already initialized." + return 0 + fi + fi + + if [ -z "$_ACME_CURL" ] && _exists "curl"; then + _ACME_CURL="curl -L --silent --dump-header $HTTP_HEADER " + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _CURL_DUMP="$(_mktemp)" + _ACME_CURL="$_ACME_CURL --trace-ascii $_CURL_DUMP " + fi + + if [ "$CA_BUNDLE" ]; then + _ACME_CURL="$_ACME_CURL --cacert $CA_BUNDLE " + fi + + fi + + if [ -z "$_ACME_WGET" ] && _exists "wget"; then + _ACME_WGET="wget -q" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _ACME_WGET="$_ACME_WGET -d " + fi + if [ "$CA_BUNDLE" ]; then + _ACME_WGET="$_ACME_WGET --ca-certificate $CA_BUNDLE " + fi + fi + + __HTTP_INITIALIZED=1 + +} + +# body url [needbase64] [POST|PUT] +_post() { + body="$1" + url="$2" + needbase64="$3" + httpmethod="$4" + + if [ -z "$httpmethod" ]; then + httpmethod="POST" + fi + _debug $httpmethod + _debug "url" "$url" + _debug2 "body" "$body" + + _inithttp + + if [ "$_ACME_CURL" ]; then + _CURL="$_ACME_CURL" + if [ "$HTTPS_INSECURE" ]; then + _CURL="$_CURL --insecure " + fi + _debug "_CURL" "$_CURL" + if [ "$needbase64" ]; then + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$url" | _base64)" + else + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$url")" + fi + _ret="$?" + if [ "$_ret" != "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _err "Here is the curl dump log:" + _err "$(cat "$_CURL_DUMP")" + fi + fi + elif [ "$_ACME_WGET" ]; then + _WGET="$_ACME_WGET" + if [ "$HTTPS_INSECURE" ]; then + _WGET="$_WGET --no-check-certificate " + fi + _debug "_WGET" "$_WGET" + if [ "$needbase64" ]; then + if [ "$httpmethod" = "POST" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$url" 2>"$HTTP_HEADER" | _base64)" + else + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$url" 2>"$HTTP_HEADER" | _base64)" + fi + else + if [ "$httpmethod" = "POST" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$url" 2>"$HTTP_HEADER")" + else + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$url" 2>"$HTTP_HEADER")" + fi + fi + _ret="$?" + if [ "$_ret" = "8" ]; then + _ret=0 + _debug "wget returns 8, the server returns a 'Bad request' respons, lets process the response later." + fi + if [ "$_ret" != "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" + fi + _sed_i "s/^ *//g" "$HTTP_HEADER" + else + _ret="$?" + _err "Neither curl nor wget is found, can not do $httpmethod." + fi + _debug "_ret" "$_ret" + printf "%s" "$response" + return $_ret +} + +# url getheader timeout +_get() { + _debug GET + url="$1" + onlyheader="$2" + t="$3" + _debug url "$url" + _debug "timeout" "$t" + + _inithttp + + if [ "$_ACME_CURL" ]; then + _CURL="$_ACME_CURL" + if [ "$HTTPS_INSECURE" ]; then + _CURL="$_CURL --insecure " + fi + if [ "$t" ]; then + _CURL="$_CURL --connect-timeout $t" + fi + _debug "_CURL" "$_CURL" + if [ "$onlyheader" ]; then + $_CURL -I --user-agent "$USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url" + else + $_CURL --user-agent "$USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url" + fi + ret=$? + if [ "$ret" != "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _err "Here is the curl dump log:" + _err "$(cat "$_CURL_DUMP")" + fi + fi + elif [ "$_ACME_WGET" ]; then + _WGET="$_ACME_WGET" + if [ "$HTTPS_INSECURE" ]; then + _WGET="$_WGET --no-check-certificate " + fi + if [ "$t" ]; then + _WGET="$_WGET --timeout=$t" + fi + _debug "_WGET" "$_WGET" + if [ "$onlyheader" ]; then + $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O /dev/null "$url" 2>&1 | sed 's/^[ ]*//g' + else + $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -O - "$url" + fi + ret=$? + if [ "$_ret" = "8" ]; then + _ret=0 + _debug "wget returns 8, the server returns a 'Bad request' respons, lets process the response later." + fi + if [ "$ret" != "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $ret" + fi + else + ret=$? + _err "Neither curl nor wget is found, can not do GET." + fi + _debug "ret" "$ret" + return $ret +} + +_head_n() { + head -n "$1" +} + +_tail_n() { + if ! tail -n "$1" 2>/dev/null; then + #fix for solaris + tail -"$1" + fi +} + +# url payload needbase64 keyfile +_send_signed_request() { + url=$1 + payload=$2 + needbase64=$3 + keyfile=$4 + if [ -z "$keyfile" ]; then + keyfile="$ACCOUNT_KEY_PATH" + fi + _debug url "$url" + _debug payload "$payload" + + if ! _calcjwk "$keyfile"; then + return 1 + fi + + payload64=$(printf "%s" "$payload" | _base64 | _urlencode) + _debug3 payload64 "$payload64" + + if [ -z "$_CACHED_NONCE" ]; then + _debug2 "Get nonce." + nonceurl="$API/directory" + _headers="$(_get "$nonceurl" "onlyheader")" + + if [ "$?" != "0" ]; then + _err "Can not connect to $nonceurl to get nonce." + return 1 + fi + + _debug2 _headers "$_headers" + + _CACHED_NONCE="$(echo "$_headers" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + _debug2 _CACHED_NONCE "$_CACHED_NONCE" + else + _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" + fi + nonce="$_CACHED_NONCE" + _debug2 nonce "$nonce" + + protected="$JWK_HEADERPLACE_PART1$nonce$JWK_HEADERPLACE_PART2" + _debug3 protected "$protected" + + protected64="$(printf "%s" "$protected" | _base64 | _urlencode)" + _debug3 protected64 "$protected64" + + if ! _sig_t="$(printf "%s" "$protected64.$payload64" | _sign "$keyfile" "sha256")"; then + _err "Sign request failed." + return 1 + fi + _debug3 _sig_t "$_sig_t" + + sig="$(printf "%s" "$_sig_t" | _urlencode)" + _debug3 sig "$sig" + + body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + _debug3 body "$body" + + response="$(_post "$body" "$url" "$needbase64")" + _CACHED_NONCE="" + if [ "$?" != "0" ]; then + _err "Can not post to $url" + return 1 + fi + _debug2 original "$response" + + response="$(echo "$response" | _normalizeJson)" + + responseHeaders="$(cat "$HTTP_HEADER")" + + _debug2 responseHeaders "$responseHeaders" + _debug2 response "$response" + code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug code "$code" + + _CACHED_NONCE="$(echo "$responseHeaders" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + +} + +#setopt "file" "opt" "=" "value" [";"] +_setopt() { + __conf="$1" + __opt="$2" + __sep="$3" + __val="$4" + __end="$5" + if [ -z "$__opt" ]; then + _usage usage: _setopt '"file" "opt" "=" "value" [";"]' + return + fi + if [ ! -f "$__conf" ]; then + touch "$__conf" + fi + + if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then + _debug3 OK + if _contains "$__val" "&"; then + __val="$(echo "$__val" | sed 's/&/\\&/g')" + fi + text="$(cat "$__conf")" + echo "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + + elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then + if _contains "$__val" "&"; then + __val="$(echo "$__val" | sed 's/&/\\&/g')" + fi + text="$(cat "$__conf")" + echo "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + + else + _debug3 APP + echo "$__opt$__sep$__val$__end" >>"$__conf" + fi + _debug2 "$(grep -n "^$__opt$__sep" "$__conf")" +} + +#_save_conf file key value +#save to conf +_save_conf() { + _s_c_f="$1" + _sdkey="$2" + _sdvalue="$3" + if [ "$_s_c_f" ]; then + _setopt "$_s_c_f" "$_sdkey" "=" "'$_sdvalue'" + else + _err "config file is empty, can not save $_sdkey=$_sdvalue" + fi +} + +#_clear_conf file key +_clear_conf() { + _c_c_f="$1" + _sdkey="$2" + if [ "$_c_c_f" ]; then + _conf_data="$(cat "$_c_c_f")" + echo "$_conf_data" | sed "s/^$_sdkey *=.*$//" >"$_c_c_f" + else + _err "config file is empty, can not clear" + fi +} + +#_read_conf file key +_read_conf() { + _r_c_f="$1" + _sdkey="$2" + if [ -f "$_r_c_f" ]; then + ( + eval "$(grep "^$_sdkey *=" "$_r_c_f")" + eval "printf \"%s\" \"\$$_sdkey\"" + ) + else + _debug "config file is empty, can not read $_sdkey" + fi +} + +#_savedomainconf key value +#save to domain.conf +_savedomainconf() { + _save_conf "$DOMAIN_CONF" "$1" "$2" +} + +#_cleardomainconf key +_cleardomainconf() { + _clear_conf "$DOMAIN_CONF" "$1" +} + +#_readdomainconf key +_readdomainconf() { + _read_conf "$DOMAIN_CONF" "$1" +} + +#_saveaccountconf key value +_saveaccountconf() { + _save_conf "$ACCOUNT_CONF_PATH" "$1" "$2" +} + +#_clearaccountconf key +_clearaccountconf() { + _clear_conf "$ACCOUNT_CONF_PATH" "$1" +} + +#_savecaconf key value +_savecaconf() { + _save_conf "$CA_CONF" "$1" "$2" +} + +#_readcaconf key +_readcaconf() { + _read_conf "$CA_CONF" "$1" +} + +#_clearaccountconf key +_clearcaconf() { + _clear_conf "$CA_CONF" "$1" +} + +# content localaddress +_startserver() { + content="$1" + ncaddr="$2" + _debug "ncaddr" "$ncaddr" + + _debug "startserver: $$" + nchelp="$(nc -h 2>&1)" + + _debug Le_HTTPPort "$Le_HTTPPort" + _debug Le_Listen_V4 "$Le_Listen_V4" + _debug Le_Listen_V6 "$Le_Listen_V6" + _NC="nc" + + if [ "$Le_Listen_V4" ]; then + _NC="$_NC -4" + elif [ "$Le_Listen_V6" ]; then + _NC="$_NC -6" + fi + + if echo "$nchelp" | grep "\-q[ ,]" >/dev/null; then + _NC="$_NC -q 1 -l $ncaddr" + else + if echo "$nchelp" | grep "GNU netcat" >/dev/null && echo "$nchelp" | grep "\-c, \-\-close" >/dev/null; then + _NC="$_NC -c -l $ncaddr" + elif echo "$nchelp" | grep "\-N" | grep "Shutdown the network socket after EOF on stdin" >/dev/null; then + _NC="$_NC -N -l $ncaddr" + else + _NC="$_NC -l $ncaddr" + fi + fi + + _debug "_NC" "$_NC" + + #for centos ncat + if _contains "$nchelp" "nmap.org"; then + _debug "Using ncat: nmap.org" + if ! _exec "printf \"%s\r\n\r\n%s\" \"HTTP/1.1 200 OK\" \"$content\" | $_NC \"$Le_HTTPPort\" >&2"; then + _exec_err + return 1 + fi + if [ "$DEBUG" ]; then + _exec_err + fi + return + fi + + # while true ; do + if ! _exec "printf \"%s\r\n\r\n%s\" \"HTTP/1.1 200 OK\" \"$content\" | $_NC -p \"$Le_HTTPPort\" >&2"; then + _exec "printf \"%s\r\n\r\n%s\" \"HTTP/1.1 200 OK\" \"$content\" | $_NC \"$Le_HTTPPort\" >&2" + fi + + if [ "$?" != "0" ]; then + _err "nc listen error." + _exec_err + exit 1 + fi + if [ "$DEBUG" ]; then + _exec_err + fi + # done +} + +_stopserver() { + pid="$1" + _debug "pid" "$pid" + if [ -z "$pid" ]; then + return + fi + + _debug2 "Le_HTTPPort" "$Le_HTTPPort" + if [ "$Le_HTTPPort" ]; then + if [ "$DEBUG" ] && [ "$DEBUG" -gt "3" ]; then + _get "http://localhost:$Le_HTTPPort" "" 1 + else + _get "http://localhost:$Le_HTTPPort" "" 1 >/dev/null 2>&1 + fi + fi + + _debug2 "Le_TLSPort" "$Le_TLSPort" + if [ "$Le_TLSPort" ]; then + if [ "$DEBUG" ] && [ "$DEBUG" -gt "3" ]; then + _get "https://localhost:$Le_TLSPort" "" 1 + _get "https://localhost:$Le_TLSPort" "" 1 + else + _get "https://localhost:$Le_TLSPort" "" 1 >/dev/null 2>&1 + _get "https://localhost:$Le_TLSPort" "" 1 >/dev/null 2>&1 + fi + fi +} + +# sleep sec +_sleep() { + _sleep_sec="$1" + if [ "$__INTERACTIVE" ]; then + _sleep_c="$_sleep_sec" + while [ "$_sleep_c" -ge "0" ]; do + printf "\r \r" + __green "$_sleep_c" + _sleep_c="$(_math "$_sleep_c" - 1)" + sleep 1 + done + printf "\r" + else + sleep "$_sleep_sec" + fi +} + +# _starttlsserver san_a san_b port content _ncaddr +_starttlsserver() { + _info "Starting tls server." + san_a="$1" + san_b="$2" + port="$3" + content="$4" + opaddr="$5" + + _debug san_a "$san_a" + _debug san_b "$san_b" + _debug port "$port" + + #create key TLS_KEY + if ! _createkey "2048" "$TLS_KEY"; then + _err "Create tls validation key error." + return 1 + fi + + #create csr + alt="$san_a" + if [ "$san_b" ]; then + alt="$alt,$san_b" + fi + if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF"; then + _err "Create tls validation csr error." + return 1 + fi + + #self signed + if ! _signcsr "$TLS_KEY" "$TLS_CSR" "$TLS_CONF" "$TLS_CERT"; then + _err "Create tls validation cert error." + return 1 + fi + + __S_OPENSSL="$OPENSSL_BIN s_server -cert $TLS_CERT -key $TLS_KEY " + if [ "$opaddr" ]; then + __S_OPENSSL="$__S_OPENSSL -accept $opaddr:$port" + else + __S_OPENSSL="$__S_OPENSSL -accept $port" + fi + + _debug Le_Listen_V4 "$Le_Listen_V4" + _debug Le_Listen_V6 "$Le_Listen_V6" + if [ "$Le_Listen_V4" ]; then + __S_OPENSSL="$__S_OPENSSL -4" + elif [ "$Le_Listen_V6" ]; then + __S_OPENSSL="$__S_OPENSSL -6" + fi + + _debug "$__S_OPENSSL" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + (printf "%s\r\n\r\n%s" "HTTP/1.1 200 OK" "$content" | $__S_OPENSSL -tlsextdebug) & + else + (printf "%s\r\n\r\n%s" "HTTP/1.1 200 OK" "$content" | $__S_OPENSSL >/dev/null 2>&1) & + fi + + serverproc="$!" + sleep 1 + _debug serverproc "$serverproc" +} + +#file +_readlink() { + _rf="$1" + if ! readlink -f "$_rf" 2>/dev/null; then + if _startswith "$_rf" "\./$PROJECT_ENTRY"; then + printf -- "%s" "$(pwd)/$PROJECT_ENTRY" + return 0 + fi + readlink "$_rf" + fi +} + +__initHome() { + if [ -z "$_SCRIPT_HOME" ]; then + if _exists readlink && _exists dirname; then + _debug "Lets find script dir." + _debug "_SCRIPT_" "$_SCRIPT_" + _script="$(_readlink "$_SCRIPT_")" + _debug "_script" "$_script" + _script_home="$(dirname "$_script")" + _debug "_script_home" "$_script_home" + if [ -d "$_script_home" ]; then + _SCRIPT_HOME="$_script_home" + else + _err "It seems the script home is not correct:$_script_home" + fi + fi + fi + + # if [ -z "$LE_WORKING_DIR" ]; then + # if [ -f "$DEFAULT_INSTALL_HOME/account.conf" ]; then + # _debug "It seems that $PROJECT_NAME is already installed in $DEFAULT_INSTALL_HOME" + # LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" + # else + # LE_WORKING_DIR="$_SCRIPT_HOME" + # fi + # fi + + if [ -z "$LE_WORKING_DIR" ]; then + _debug "Using default home:$DEFAULT_INSTALL_HOME" + LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" + fi + export LE_WORKING_DIR + + _DEFAULT_ACCOUNT_CONF_PATH="$LE_WORKING_DIR/account.conf" + + if [ -z "$ACCOUNT_CONF_PATH" ]; then + if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then + . "$_DEFAULT_ACCOUNT_CONF_PATH" + fi + fi + + if [ -z "$ACCOUNT_CONF_PATH" ]; then + ACCOUNT_CONF_PATH="$_DEFAULT_ACCOUNT_CONF_PATH" + fi + + DEFAULT_LOG_FILE="$LE_WORKING_DIR/$PROJECT_NAME.log" + + DEFAULT_CA_HOME="$LE_WORKING_DIR/ca" + + if [ -z "$LE_TEMP_DIR" ]; then + LE_TEMP_DIR="$LE_WORKING_DIR/tmp" + fi +} + +#[domain] [keylength] +_initpath() { + + __initHome + + if [ -f "$ACCOUNT_CONF_PATH" ]; then + . "$ACCOUNT_CONF_PATH" + fi + + if [ "$IN_CRON" ]; then + if [ ! "$_USER_PATH_EXPORTED" ]; then + _USER_PATH_EXPORTED=1 + export PATH="$USER_PATH:$PATH" + fi + fi + + if [ -z "$CA_HOME" ]; then + CA_HOME="$DEFAULT_CA_HOME" + fi + + if [ -z "$API" ]; then + if [ -z "$STAGE" ]; then + API="$DEFAULT_CA" + else + API="$STAGE_CA" + _info "Using stage api:$API" + fi + fi + + _API_HOST="$(echo "$API" | cut -d : -f 2 | tr -d '/')" + CA_DIR="$CA_HOME/$_API_HOST" + + _DEFAULT_CA_CONF="$CA_DIR/ca.conf" + + if [ -z "$CA_CONF" ]; then + CA_CONF="$_DEFAULT_CA_CONF" + fi + _debug3 CA_CONF "$CA_CONF" + + if [ -f "$CA_CONF" ]; then + . "$CA_CONF" + fi + + if [ -z "$ACME_DIR" ]; then + ACME_DIR="/home/.acme" + fi + + if [ -z "$APACHE_CONF_BACKUP_DIR" ]; then + APACHE_CONF_BACKUP_DIR="$LE_WORKING_DIR" + fi + + if [ -z "$USER_AGENT" ]; then + USER_AGENT="$DEFAULT_USER_AGENT" + fi + + if [ -z "$HTTP_HEADER" ]; then + HTTP_HEADER="$LE_WORKING_DIR/http.header" + fi + + _OLD_ACCOUNT_KEY="$LE_WORKING_DIR/account.key" + _OLD_ACCOUNT_JSON="$LE_WORKING_DIR/account.json" + + _DEFAULT_ACCOUNT_KEY_PATH="$CA_DIR/account.key" + _DEFAULT_ACCOUNT_JSON_PATH="$CA_DIR/account.json" + if [ -z "$ACCOUNT_KEY_PATH" ]; then + ACCOUNT_KEY_PATH="$_DEFAULT_ACCOUNT_KEY_PATH" + fi + + if [ -z "$ACCOUNT_JSON_PATH" ]; then + ACCOUNT_JSON_PATH="$_DEFAULT_ACCOUNT_JSON_PATH" + fi + + _DEFAULT_CERT_HOME="$LE_WORKING_DIR" + if [ -z "$CERT_HOME" ]; then + CERT_HOME="$_DEFAULT_CERT_HOME" + fi + + if [ -z "$OPENSSL_BIN" ]; then + OPENSSL_BIN="$DEFAULT_OPENSSL_BIN" + fi + + if [ -z "$1" ]; then + return 0 + fi + + domain="$1" + _ilength="$2" + + if [ -z "$DOMAIN_PATH" ]; then + domainhome="$CERT_HOME/$domain" + domainhomeecc="$CERT_HOME/$domain$ECC_SUFFIX" + + DOMAIN_PATH="$domainhome" + + if _isEccKey "$_ilength"; then + DOMAIN_PATH="$domainhomeecc" + else + if [ ! -d "$domainhome" ] && [ -d "$domainhomeecc" ]; then + _info "The domain '$domain' seems to have a ECC cert already, please add '$(__red "--ecc")' parameter if you want to use that cert." + fi + fi + _debug DOMAIN_PATH "$DOMAIN_PATH" + fi + + if [ -z "$DOMAIN_CONF" ]; then + DOMAIN_CONF="$DOMAIN_PATH/$domain.conf" + fi + + if [ -z "$DOMAIN_SSL_CONF" ]; then + DOMAIN_SSL_CONF="$DOMAIN_PATH/$domain.csr.conf" + fi + + if [ -z "$CSR_PATH" ]; then + CSR_PATH="$DOMAIN_PATH/$domain.csr" + fi + if [ -z "$CERT_KEY_PATH" ]; then + CERT_KEY_PATH="$DOMAIN_PATH/$domain.key" + fi + if [ -z "$CERT_PATH" ]; then + CERT_PATH="$DOMAIN_PATH/$domain.cer" + fi + if [ -z "$CA_CERT_PATH" ]; then + CA_CERT_PATH="$DOMAIN_PATH/ca.cer" + fi + if [ -z "$CERT_FULLCHAIN_PATH" ]; then + CERT_FULLCHAIN_PATH="$DOMAIN_PATH/fullchain.cer" + fi + if [ -z "$CERT_PFX_PATH" ]; then + CERT_PFX_PATH="$DOMAIN_PATH/$domain.pfx" + fi + + if [ -z "$TLS_CONF" ]; then + TLS_CONF="$DOMAIN_PATH/tls.valdation.conf" + fi + if [ -z "$TLS_CERT" ]; then + TLS_CERT="$DOMAIN_PATH/tls.valdation.cert" + fi + if [ -z "$TLS_KEY" ]; then + TLS_KEY="$DOMAIN_PATH/tls.valdation.key" + fi + if [ -z "$TLS_CSR" ]; then + TLS_CSR="$DOMAIN_PATH/tls.valdation.csr" + fi + +} + +_exec() { + if [ -z "$_EXEC_TEMP_ERR" ]; then + _EXEC_TEMP_ERR="$(_mktemp)" + fi + + if [ "$_EXEC_TEMP_ERR" ]; then + eval "$@ 2>>$_EXEC_TEMP_ERR" + else + eval "$@" + fi +} + +_exec_err() { + [ "$_EXEC_TEMP_ERR" ] && _err "$(cat "$_EXEC_TEMP_ERR")" && echo "" >"$_EXEC_TEMP_ERR" +} + +_apachePath() { + _APACHECTL="apachectl" + if ! _exists apachectl; then + if _exists apache2ctl; then + _APACHECTL="apache2ctl" + else + _err "'apachectl not found. It seems that apache is not installed, or you are not root user.'" + _err "Please use webroot mode to try again." + return 1 + fi + fi + + if ! _exec $_APACHECTL -V >/dev/null; then + _exec_err + return 1 + fi + + if [ "$APACHE_HTTPD_CONF" ]; then + _saveaccountconf APACHE_HTTPD_CONF "$APACHE_HTTPD_CONF" + httpdconf="$APACHE_HTTPD_CONF" + httpdconfname="$(basename "$httpdconfname")" + else + httpdconfname="$($_APACHECTL -V | grep SERVER_CONFIG_FILE= | cut -d = -f 2 | tr -d '"')" + _debug httpdconfname "$httpdconfname" + + if [ -z "$httpdconfname" ]; then + _err "Can not read apache config file." + return 1 + fi + + if _startswith "$httpdconfname" '/'; then + httpdconf="$httpdconfname" + httpdconfname="$(basename "$httpdconfname")" + else + httpdroot="$($_APACHECTL -V | grep HTTPD_ROOT= | cut -d = -f 2 | tr -d '"')" + _debug httpdroot "$httpdroot" + httpdconf="$httpdroot/$httpdconfname" + httpdconfname="$(basename "$httpdconfname")" + fi + fi + _debug httpdconf "$httpdconf" + _debug httpdconfname "$httpdconfname" + if [ ! -f "$httpdconf" ]; then + _err "Apache Config file not found" "$httpdconf" + return 1 + fi + return 0 +} + +_restoreApache() { + if [ -z "$usingApache" ]; then + return 0 + fi + _initpath + if ! _apachePath; then + return 1 + fi + + if [ ! -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" ]; then + _debug "No config file to restore." + return 0 + fi + + cat "$APACHE_CONF_BACKUP_DIR/$httpdconfname" >"$httpdconf" + _debug "Restored: $httpdconf." + if ! _exec $_APACHECTL -t; then + _exec_err + _err "Sorry, restore apache config error, please contact me." + return 1 + fi + _debug "Restored successfully." + rm -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" + return 0 +} + +_setApache() { + _initpath + if ! _apachePath; then + return 1 + fi + + #test the conf first + _info "Checking if there is an error in the apache config file before starting." + + if ! _exec "$_APACHECTL" -t >/dev/null; then + _exec_err + _err "The apache config file has error, please fix it first, then try again." + _err "Don't worry, there is nothing changed to your system." + return 1 + else + _info "OK" + fi + + #backup the conf + _debug "Backup apache config file" "$httpdconf" + if ! cp "$httpdconf" "$APACHE_CONF_BACKUP_DIR/"; then + _err "Can not backup apache config file, so abort. Don't worry, the apache config is not changed." + _err "This might be a bug of $PROJECT_NAME , pleae report issue: $PROJECT" + return 1 + fi + _info "JFYI, Config file $httpdconf is backuped to $APACHE_CONF_BACKUP_DIR/$httpdconfname" + _info "In case there is an error that can not be restored automatically, you may try restore it yourself." + _info "The backup file will be deleted on sucess, just forget it." + + #add alias + + apacheVer="$($_APACHECTL -V | grep "Server version:" | cut -d : -f 2 | cut -d " " -f 2 | cut -d '/' -f 2)" + _debug "apacheVer" "$apacheVer" + apacheMajer="$(echo "$apacheVer" | cut -d . -f 1)" + apacheMinor="$(echo "$apacheVer" | cut -d . -f 2)" + + if [ "$apacheVer" ] && [ "$apacheMajer$apacheMinor" -ge "24" ]; then + echo " +Alias /.well-known/acme-challenge $ACME_DIR + + +Require all granted + + " >>"$httpdconf" + else + echo " +Alias /.well-known/acme-challenge $ACME_DIR + + +Order allow,deny +Allow from all + + " >>"$httpdconf" + fi + + _msg="$($_APACHECTL -t 2>&1)" + if [ "$?" != "0" ]; then + _err "Sorry, apache config error" + if _restoreApache; then + _err "The apache config file is restored." + else + _err "Sorry, The apache config file can not be restored, please report bug." + fi + return 1 + fi + + if [ ! -d "$ACME_DIR" ]; then + mkdir -p "$ACME_DIR" + chmod 755 "$ACME_DIR" + fi + + if ! _exec "$_APACHECTL" graceful; then + _exec_err + _err "$_APACHECTL graceful error, please contact me." + _restoreApache + return 1 + fi + usingApache="1" + return 0 +} + +_clearup() { + _stopserver "$serverproc" + serverproc="" + _restoreApache + _clearupdns + if [ -z "$DEBUG" ]; then + rm -f "$TLS_CONF" + rm -f "$TLS_CERT" + rm -f "$TLS_KEY" + rm -f "$TLS_CSR" + fi +} + +_clearupdns() { + _debug "_clearupdns" + if [ "$dnsadded" != 1 ] || [ -z "$vlist" ]; then + _debug "Dns not added, skip." + return + fi + + ventries=$(echo "$vlist" | tr ',' ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _urlencode)" + _debug txt "$txt" + if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then + _info "$d is already verified, skip $vtype." + continue + fi + + if [ "$vtype" != "$VTYPE_DNS" ]; then + _info "Skip $d for $vtype" + continue + fi + + d_api="$(_findHook "$d" dnsapi "$_currentRoot")" + _debug d_api "$d_api" + + if [ -z "$d_api" ]; then + _info "Not Found domain api file: $d_api" + continue + fi + + ( + if ! . "$d_api"; then + _err "Load file $d_api error. Please check your api file and try again." + return 1 + fi + + rmcommand="${_currentRoot}_rm" + if ! _exists "$rmcommand"; then + _err "It seems that your api file doesn't define $rmcommand" + return 1 + fi + + txtdomain="_acme-challenge.$d" + + if ! $rmcommand "$txtdomain" "$txt"; then + _err "Error removing txt for domain:$txtdomain" + return 1 + fi + ) + + done +} + +# webroot removelevel tokenfile +_clearupwebbroot() { + __webroot="$1" + __domain="$4" + if [ -z "$__webroot" ]; then + _debug "no webroot specified, skip" + return 0 + fi + + h_api="$(_findHook "$d" httpapi "$_currentRoot")" + _debug h_api "$h_api" + + if [ "$h_api" ]; then + _info "Found domain http api file: $h_api" + ( + if ! . "$h_api"; then + _err "Load file $h_api error. Please check your api file and try again." + return 1 + fi + + rmcommand="${_currentRoot}_rm" + if ! _exists "$rmcommand"; then + _err "It seems that your api file is not correct, it must have a function named: $rmcommand" + return 1 + fi + + if ! $rmcommand "$__domain" "$3"; then + _err "Error rm webroot api for domain:$__webroot" + return 1 + fi + ) + + else + _rmpath="" + if [ "$2" = '1' ]; then + _rmpath="$__webroot/.well-known" + elif [ "$2" = '2' ]; then + _rmpath="$__webroot/.well-known/acme-challenge" + elif [ "$2" = '3' ]; then + _rmpath="$__webroot/.well-known/acme-challenge/$3" + else + _debug "Skip for removelevel:$2" + fi + + if [ "$_rmpath" ]; then + if [ "$DEBUG" ]; then + _debug "Debugging, skip removing: $_rmpath" + else + rm -rf "$_rmpath" + fi + fi + fi + return 0 + +} + +_on_before_issue() { + _debug _on_before_issue + if _hasfield "$Le_Webroot" "$NO_VALUE"; then + if ! _exists "nc"; then + _err "Please install netcat(nc) tools first." + return 1 + fi + fi + + _debug Le_LocalAddress "$Le_LocalAddress" + + alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ') + _index=1 + _currentRoot="" + _addrIndex=1 + for d in $alldomains; do + _debug "Check for domain" "$d" + _currentRoot="$(_getfield "$Le_Webroot" $_index)" + _debug "_currentRoot" "$_currentRoot" + _index=$(_math $_index + 1) + _checkport="" + if [ "$_currentRoot" = "$NO_VALUE" ]; then + _info "Standalone mode." + if [ -z "$Le_HTTPPort" ]; then + Le_HTTPPort=80 + else + _savedomainconf "Le_HTTPPort" "$Le_HTTPPort" + fi + _checkport="$Le_HTTPPort" + elif [ "$_currentRoot" = "$W_TLS" ]; then + _info "Standalone tls mode." + if [ -z "$Le_TLSPort" ]; then + Le_TLSPort=443 + else + _savedomainconf "Le_TLSPort" "$Le_TLSPort" + fi + _checkport="$Le_TLSPort" + fi + + if [ "$_checkport" ]; then + _debug _checkport "$_checkport" + _checkaddr="$(_getfield "$Le_LocalAddress" $_addrIndex)" + _debug _checkaddr "$_checkaddr" + + _addrIndex="$(_math $_addrIndex + 1)" + + _netprc="$(_ss "$_checkport" | grep "$_checkport")" + netprc="$(echo "$_netprc" | grep "$_checkaddr")" + if [ -z "$netprc" ]; then + netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS")" + fi + if [ "$netprc" ]; then + _err "$netprc" + _err "tcp port $_checkport is already used by $(echo "$netprc" | cut -d : -f 4)" + _err "Please stop it first" + return 1 + fi + fi + done + + if _hasfield "$Le_Webroot" "apache"; then + if ! _setApache; then + _err "set up apache error. Report error to me." + return 1 + fi + else + usingApache="" + fi + + #run pre hook + if [ "$Le_PreHook" ]; then + _info "Run pre hook:'$Le_PreHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_PreHook" + ); then + _err "Error when run pre hook." + return 1 + fi + fi +} + +_on_issue_err() { + _debug _on_issue_err + if [ "$LOG_FILE" ]; then + _err "Please check log file for more details: $LOG_FILE" + else + _err "Please add '--debug' or '--log' to check more details." + _err "See: $_DEBUG_WIKI" + fi + + if [ "$DEBUG" ] && [ "$DEBUG" -gt "0" ]; then + _debug "$(_dlg_versions)" + fi + + #run the post hook + if [ "$Le_PostHook" ]; then + _info "Run post hook:'$Le_PostHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_PostHook" + ); then + _err "Error when run post hook." + return 1 + fi + fi +} + +_on_issue_success() { + _debug _on_issue_success + #run the post hook + if [ "$Le_PostHook" ]; then + _info "Run post hook:'$Le_PostHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_PostHook" + ); then + _err "Error when run post hook." + return 1 + fi + fi + + #run renew hook + if [ "$IS_RENEW" ] && [ "$Le_RenewHook" ]; then + _info "Run renew hook:'$Le_RenewHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_RenewHook" + ); then + _err "Error when run renew hook." + return 1 + fi + fi + +} + +updateaccount() { + _initpath + _regAccount +} + +registeraccount() { + _reg_length="$1" + _initpath + _regAccount "$_reg_length" +} + +__calcAccountKeyHash() { + [ -f "$ACCOUNT_KEY_PATH" ] && _digest sha256 <"$ACCOUNT_KEY_PATH" +} + +#keylength +_regAccount() { + _initpath + _reg_length="$1" + + if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then + _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH" + mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH" + fi + + if [ ! -f "$ACCOUNT_JSON_PATH" ] && [ -f "$_OLD_ACCOUNT_JSON" ]; then + _info "mv $_OLD_ACCOUNT_JSON to $ACCOUNT_JSON_PATH" + mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH" + fi + + if [ ! -f "$ACCOUNT_KEY_PATH" ]; then + if ! _create_account_key "$_reg_length"; then + _err "Create account key error." + return 1 + fi + fi + + if ! _calcjwk "$ACCOUNT_KEY_PATH"; then + return 1 + fi + + _updateTos="" + _reg_res="new-reg" + while true; do + _debug AGREEMENT "$AGREEMENT" + + regjson='{"resource": "'$_reg_res'", "agreement": "'$AGREEMENT'"}' + + if [ "$ACCOUNT_EMAIL" ]; then + regjson='{"resource": "'$_reg_res'", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' + fi + + if [ -z "$_updateTos" ]; then + _info "Registering account" + + if ! _send_signed_request "$API/acme/new-reg" "$regjson"; then + _err "Register account Error: $response" + return 1 + fi + + if [ "$code" = "" ] || [ "$code" = '201' ]; then + echo "$response" >"$ACCOUNT_JSON_PATH" + _info "Registered" + elif [ "$code" = '409' ]; then + _info "Already registered" + else + _err "Register account Error: $response" + return 1 + fi + + _accUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" + _debug "_accUri" "$_accUri" + + _tos="$(echo "$responseHeaders" | grep "^Link:.*rel=\"terms-of-service\"" | _head_n 1 | _egrep_o "<.*>" | tr -d '<>')" + _debug "_tos" "$_tos" + if [ -z "$_tos" ]; then + _debug "Use default tos: $DEFAULT_AGREEMENT" + _tos="$DEFAULT_AGREEMENT" + fi + if [ "$_tos" != "$AGREEMENT" ]; then + _updateTos=1 + AGREEMENT="$_tos" + _reg_res="reg" + continue + fi + + else + _debug "Update tos: $_tos" + if ! _send_signed_request "$_accUri" "$regjson"; then + _err "Update tos error." + return 1 + fi + if [ "$code" = '202' ]; then + _info "Update success." + + CA_KEY_HASH="$(__calcAccountKeyHash)" + _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" + _savecaconf CA_KEY_HASH "$CA_KEY_HASH" + else + _err "Update account error." + return 1 + fi + fi + return 0 + done + +} + +# domain folder file +_findHook() { + _hookdomain="$1" + _hookcat="$2" + _hookname="$3" + + if [ -f "$_SCRIPT_HOME/$_hookcat/$_hookname" ]; then + d_api="$_SCRIPT_HOME/$_hookcat/$_hookname" + elif [ -f "$_SCRIPT_HOME/$_hookcat/$_hookname.sh" ]; then + d_api="$_SCRIPT_HOME/$_hookcat/$_hookname.sh" + elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname" ]; then + d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname" + elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" ]; then + d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" + elif [ -f "$LE_WORKING_DIR/$_hookname" ]; then + d_api="$LE_WORKING_DIR/$_hookname" + elif [ -f "$LE_WORKING_DIR/$_hookname.sh" ]; then + d_api="$LE_WORKING_DIR/$_hookname.sh" + elif [ -f "$LE_WORKING_DIR/$_hookcat/$_hookname" ]; then + d_api="$LE_WORKING_DIR/$_hookcat/$_hookname" + elif [ -f "$LE_WORKING_DIR/$_hookcat/$_hookname.sh" ]; then + d_api="$LE_WORKING_DIR/$_hookcat/$_hookname.sh" + fi + + printf "%s" "$d_api" +} + +#domain +__get_domain_new_authz() { + _gdnd="$1" + _info "Getting new-authz for domain" "$_gdnd" + + _Max_new_authz_retry_times=5 + _authz_i=0 + while [ "$_authz_i" -lt "$_Max_new_authz_retry_times" ]; do + _debug "Try new-authz for the $_authz_i time." + if ! _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$(_idn "$_gdnd")\"}}"; then + _err "Can not get domain new authz." + return 1 + fi + if ! _contains "$response" "An error occurred while processing your request"; then + _info "The new-authz request is ok." + break + fi + _authz_i="$(_math "$_authz_i" + 1)" + _info "The server is busy, Sleep $_authz_i to retry." + _sleep "$_authz_i" + done + + if [ "$_authz_i" = "$_Max_new_authz_retry_times" ]; then + _err "new-authz retry reach the max $_Max_new_authz_retry_times times." + fi + + if [ ! -z "$code" ] && [ ! "$code" = '201' ]; then + _err "new-authz error: $response" + return 1 + fi + +} + +#webroot, domain domainlist keylength +issue() { + if [ -z "$2" ]; then + _usage "Usage: $PROJECT_ENTRY --issue -d a.com -w /path/to/webroot/a.com/ " + return 1 + fi + Le_Webroot="$1" + Le_Domain="$2" + Le_Alt="$3" + Le_Keylength="$4" + Le_RealCertPath="$5" + Le_RealKeyPath="$6" + Le_RealCACertPath="$7" + Le_ReloadCmd="$8" + Le_RealFullChainPath="$9" + Le_PreHook="${10}" + Le_PostHook="${11}" + Le_RenewHook="${12}" + Le_LocalAddress="${13}" + + #remove these later. + if [ "$Le_Webroot" = "dns-cf" ]; then + Le_Webroot="dns_cf" + fi + if [ "$Le_Webroot" = "dns-dp" ]; then + Le_Webroot="dns_dp" + fi + if [ "$Le_Webroot" = "dns-cx" ]; then + Le_Webroot="dns_cx" + fi + _debug "Using api: $API" + + if [ ! "$IS_RENEW" ]; then + _initpath "$Le_Domain" "$Le_Keylength" + mkdir -p "$DOMAIN_PATH" + fi + + if [ -f "$DOMAIN_CONF" ]; then + Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) + _debug Le_NextRenewTime "$Le_NextRenewTime" + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _saved_domain=$(_readdomainconf Le_Domain) + _debug _saved_domain "$_saved_domain" + _saved_alt=$(_readdomainconf Le_Alt) + _debug _saved_alt "$_saved_alt" + if [ "$_saved_domain,$_saved_alt" = "$Le_Domain,$Le_Alt" ]; then + _info "Domains not changed." + _info "Skip, Next renewal time is: $(__green "$(_readdomainconf Le_NextRenewTimeStr)")" + _info "Add '$(__red '--force')' to force to renew." + return $RENEW_SKIP + else + _info "Domains have changed." + fi + fi + fi + + _savedomainconf "Le_Domain" "$Le_Domain" + _savedomainconf "Le_Alt" "$Le_Alt" + _savedomainconf "Le_Webroot" "$Le_Webroot" + + _savedomainconf "Le_PreHook" "$Le_PreHook" + _savedomainconf "Le_PostHook" "$Le_PostHook" + _savedomainconf "Le_RenewHook" "$Le_RenewHook" + + if [ "$Le_LocalAddress" ]; then + _savedomainconf "Le_LocalAddress" "$Le_LocalAddress" + else + _cleardomainconf "Le_LocalAddress" + fi + + Le_API="$API" + _savedomainconf "Le_API" "$Le_API" + + if [ "$Le_Alt" = "$NO_VALUE" ]; then + Le_Alt="" + fi + + if [ "$Le_Keylength" = "$NO_VALUE" ]; then + Le_Keylength="" + fi + + if ! _on_before_issue; then + _err "_on_before_issue." + return 1 + fi + + _saved_account_key_hash="$(_readcaconf "CA_KEY_HASH")" + _debug2 _saved_account_key_hash "$_saved_account_key_hash" + + if [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then + if ! _regAccount "$_accountkeylength"; then + _on_issue_err + return 1 + fi + else + _debug "_saved_account_key_hash is not changed, skip register account." + fi + + if [ -f "$CSR_PATH" ] && [ ! -f "$CERT_KEY_PATH" ]; then + _info "Signing from existing CSR." + else + _key=$(_readdomainconf Le_Keylength) + _debug "Read key length:$_key" + if [ ! -f "$CERT_KEY_PATH" ] || [ "$Le_Keylength" != "$_key" ]; then + if ! createDomainKey "$Le_Domain" "$Le_Keylength"; then + _err "Create domain key error." + _clearup + _on_issue_err + return 1 + fi + fi + + if ! _createcsr "$Le_Domain" "$Le_Alt" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then + _err "Create CSR error." + _clearup + _on_issue_err + return 1 + fi + fi + + _savedomainconf "Le_Keylength" "$Le_Keylength" + + vlist="$Le_Vlist" + + _info "Getting domain auth token for each domain" + sep='#' + if [ -z "$vlist" ]; then + alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ') + _index=1 + _currentRoot="" + for d in $alldomains; do + _info "Getting webroot for domain" "$d" + _w="$(echo $Le_Webroot | cut -d , -f $_index)" + _info _w "$_w" + if [ "$_w" ]; then + _currentRoot="$_w" + fi + _debug "_currentRoot" "$_currentRoot" + _index=$(_math $_index + 1) + + vtype="$VTYPE_HTTP" + if _startswith "$_currentRoot" "dns"; then + vtype="$VTYPE_DNS" + fi + + if [ "$_currentRoot" = "$W_TLS" ]; then + vtype="$VTYPE_TLS" + fi + + if ! __get_domain_new_authz "$d"; then + _clearup + _on_issue_err + return 1 + fi + + if [ -z "$thumbprint" ]; then + accountkey_json=$(printf "%s" "$jwk" | tr -d ' ') + thumbprint=$(printf "%s" "$accountkey_json" | _digest "sha256" | _urlencode) + fi + + entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + _debug entry "$entry" + if [ -z "$entry" ]; then + _err "Error, can not get domain token $d" + _clearup + _on_issue_err + return 1 + fi + token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token "$token" + + uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d : -f 2,3 | tr -d '"')" + _debug uri "$uri" + + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" + + if printf "%s" "$response" | grep '"status":"valid"' >/dev/null 2>&1; then + _info "$d is already verified, skip." + keyauthorization="$STATE_VERIFIED" + _debug keyauthorization "$keyauthorization" + fi + + dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot" + _debug dvlist "$dvlist" + + vlist="$vlist$dvlist," + + done + + #add entry + dnsadded="" + ventries=$(echo "$vlist" | tr ',' ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + + if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then + _info "$d is already verified, skip $vtype." + continue + fi + + if [ "$vtype" = "$VTYPE_DNS" ]; then + dnsadded='0' + txtdomain="_acme-challenge.$d" + _debug txtdomain "$txtdomain" + txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _urlencode)" + _debug txt "$txt" + + d_api="$(_findHook "$d" dnsapi "$_currentRoot")" + + _debug d_api "$d_api" + + if [ "$d_api" ]; then + _info "Found domain api file: $d_api" + else + _err "Add the following TXT record:" + _err "Domain: '$(__green "$txtdomain")'" + _err "TXT value: '$(__green "$txt")'" + _err "Please be aware that you prepend _acme-challenge. before your domain" + _err "so the resulting subdomain will be: $txtdomain" + continue + fi + + ( + if ! . "$d_api"; then + _err "Load file $d_api error. Please check your api file and try again." + return 1 + fi + + addcommand="${_currentRoot}_add" + if ! _exists "$addcommand"; then + _err "It seems that your api file is not correct, it must have a function named: $addcommand" + return 1 + fi + + if ! $addcommand "$txtdomain" "$txt"; then + _err "Error add txt for domain:$txtdomain" + return 1 + fi + ) + + if [ "$?" != "0" ]; then + _clearup + _on_issue_err + return 1 + fi + dnsadded='1' + fi + done + + if [ "$dnsadded" = '0' ]; then + _savedomainconf "Le_Vlist" "$vlist" + _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit." + _err "Please add the TXT records to the domains, and retry again." + _clearup + _on_issue_err + return 1 + fi + + fi + + if [ "$dnsadded" = '1' ]; then + if [ -z "$Le_DNSSleep" ]; then + Le_DNSSleep="$DEFAULT_DNS_SLEEP" + else + _savedomainconf "Le_DNSSleep" "$Le_DNSSleep" + fi + + _info "Sleep $(__green $Le_DNSSleep) seconds for the txt records to take effect" + _sleep "$Le_DNSSleep" + fi + + _debug "ok, let's start to verify" + + _ncIndex=1 + ventries=$(echo "$vlist" | tr ',' ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + uri=$(echo "$ventry" | cut -d "$sep" -f 3) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + + if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then + _info "$d is already verified, skip $vtype." + continue + fi + + _info "Verifying:$d" + _debug "d" "$d" + _debug "keyauthorization" "$keyauthorization" + _debug "uri" "$uri" + removelevel="" + token="$(printf "%s" "$keyauthorization" | cut -d '.' -f 1)" + + _debug "_currentRoot" "$_currentRoot" + + if [ "$vtype" = "$VTYPE_HTTP" ]; then + h_api="$(_findHook "$d" httpapi "$_currentRoot")" + _debug h_api "$h_api" + + if [ "$h_api" ]; then + _info "Found domain http api file: $h_api" + ( + if ! . "$h_api"; then + _err "Load file $h_api error. Please check your api file and try again." + return 1 + fi + + addcommand="${_currentRoot}_add" + if ! _exists "$addcommand"; then + _err "It seems that your api file is not correct, it must have a function named: $addcommand" + return 1 + fi + + if ! $addcommand "$d" "$token" "$keyauthorization"; then + _err "Error add txt for domain:$txtdomain" + return 1 + fi + ) + + elif [ "$_currentRoot" = "$NO_VALUE" ]; then + _info "Standalone mode server" + _ncaddr="$(_getfield "$Le_LocalAddress" "$_ncIndex")" + _ncIndex="$(_math $_ncIndex + 1)" + _startserver "$keyauthorization" "$_ncaddr" & + if [ "$?" != "0" ]; then + _clearup + _on_issue_err + return 1 + fi + serverproc="$!" + sleep 1 + _debug serverproc "$serverproc" + + else + if [ "$_currentRoot" = "apache" ]; then + wellknown_path="$ACME_DIR" + else + wellknown_path="$_currentRoot/.well-known/acme-challenge" + if [ ! -d "$_currentRoot/.well-known" ]; then + removelevel='1' + elif [ ! -d "$_currentRoot/.well-known/acme-challenge" ]; then + removelevel='2' + else + removelevel='3' + fi + fi + + _debug wellknown_path "$wellknown_path" + + _debug "writing token:$token to $wellknown_path/$token" + + mkdir -p "$wellknown_path" + + if ! printf "%s" "$keyauthorization" >"$wellknown_path/$token"; then + _err "$d:Can not write token to file : $wellknown_path/$token" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + + if [ ! "$usingApache" ]; then + if webroot_owner=$(_stat "$_currentRoot"); then + _debug "Changing owner/group of .well-known to $webroot_owner" + chown -R "$webroot_owner" "$_currentRoot/.well-known" + else + _debug "not chaning owner/group of webroot" + fi + fi + + fi + + elif [ "$vtype" = "$VTYPE_TLS" ]; then + #create A + #_hash_A="$(printf "%s" $token | _digest "sha256" "hex" )" + #_debug2 _hash_A "$_hash_A" + #_x="$(echo $_hash_A | cut -c 1-32)" + #_debug2 _x "$_x" + #_y="$(echo $_hash_A | cut -c 33-64)" + #_debug2 _y "$_y" + #_SAN_A="$_x.$_y.token.acme.invalid" + #_debug2 _SAN_A "$_SAN_A" + + #create B + _hash_B="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" + _debug2 _hash_B "$_hash_B" + _x="$(echo "$_hash_B" | cut -c 1-32)" + _debug2 _x "$_x" + _y="$(echo "$_hash_B" | cut -c 33-64)" + _debug2 _y "$_y" + + #_SAN_B="$_x.$_y.ka.acme.invalid" + + _SAN_B="$_x.$_y.acme.invalid" + _debug2 _SAN_B "$_SAN_B" + + _ncaddr="$(_getfield "$Le_LocalAddress" "$_ncIndex")" + _ncIndex="$(_math "$_ncIndex" + 1)" + if ! _starttlsserver "$_SAN_B" "$_SAN_A" "$Le_TLSPort" "$keyauthorization" "$_ncaddr"; then + _err "Start tls server error." + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + fi + + if ! _send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"; then + _err "$d:Can not get challenge: $response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + + if [ ! -z "$code" ] && [ ! "$code" = '202' ]; then + _err "$d:Challenge error: $response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + + waittimes=0 + if [ -z "$MAX_RETRY_TIMES" ]; then + MAX_RETRY_TIMES=30 + fi + + while true; do + waittimes=$(_math "$waittimes" + 1) + if [ "$waittimes" -ge "$MAX_RETRY_TIMES" ]; then + _err "$d:Timeout" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + + _debug "sleep 2 secs to verify" + sleep 2 + _debug "checking" + response="$(_get "$uri")" + if [ "$?" != "0" ]; then + _err "$d:Verify error:$response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + _debug2 original "$response" + + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + + status=$(echo "$response" | _egrep_o '"status":"[^"]*' | cut -d : -f 2 | tr -d '"') + if [ "$status" = "valid" ]; then + _info "$(__green Success)" + _stopserver "$serverproc" + serverproc="" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + break + fi + + if [ "$status" = "invalid" ]; then + error="$(echo "$response" | tr -d "\r\n" | _egrep_o '"error":\{[^\}]*')" + _debug2 error "$error" + errordetail="$(echo "$error" | _egrep_o '"detail": *"[^"]*' | cut -d '"' -f 4)" + _debug2 errordetail "$errordetail" + if [ "$errordetail" ]; then + _err "$d:Verify error:$errordetail" + else + _err "$d:Verify error:$error" + fi + if [ "$DEBUG" ]; then + if [ "$vtype" = "$VTYPE_HTTP" ]; then + _debug "Debug: get token url." + _get "http://$d/.well-known/acme-challenge/$token" "" 1 + fi + fi + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + + if [ "$status" = "pending" ]; then + _info "Pending" + else + _err "$d:Verify error:$response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" "$d" + _clearup + _on_issue_err + return 1 + fi + + done + + done + + _clearup + _info "Verify finished, start to sign." + der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _urlencode)" + + if ! _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"; then + _err "Sign failed." + _on_issue_err + return 1 + fi + + _rcert="$response" + Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" + _savedomainconf "Le_LinkCert" "$Le_LinkCert" + + if [ "$Le_LinkCert" ]; then + echo "$BEGIN_CERT" >"$CERT_PATH" + + #if ! _get "$Le_LinkCert" | _base64 "multiline" >> "$CERT_PATH" ; then + # _debug "Get cert failed. Let's try last response." + # printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >> "$CERT_PATH" + #fi + + if ! printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >>"$CERT_PATH"; then + _debug "Try cert link." + _get "$Le_LinkCert" | _base64 "multiline" >>"$CERT_PATH" + fi + + echo "$END_CERT" >>"$CERT_PATH" + _info "$(__green "Cert success.")" + cat "$CERT_PATH" + + _info "Your cert is in $(__green " $CERT_PATH ")" + + if [ -f "$CERT_KEY_PATH" ]; then + _info "Your cert key is in $(__green " $CERT_KEY_PATH ")" + fi + + cp "$CERT_PATH" "$CERT_FULLCHAIN_PATH" + + if [ ! "$USER_PATH" ] || [ ! "$IN_CRON" ]; then + USER_PATH="$PATH" + _saveaccountconf "USER_PATH" "$USER_PATH" + fi + fi + + if [ -z "$Le_LinkCert" ]; then + response="$(echo "$response" | _dbase64 "multiline" | _normalizeJson)" + _err "Sign failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" + _on_issue_err + return 1 + fi + + _cleardomainconf "Le_Vlist" + + Le_LinkIssuer=$(grep -i '^Link' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | cut -d ';' -f 1 | tr -d '<>') + if ! _contains "$Le_LinkIssuer" ":"; then + Le_LinkIssuer="$API$Le_LinkIssuer" + fi + + _savedomainconf "Le_LinkIssuer" "$Le_LinkIssuer" + + if [ "$Le_LinkIssuer" ]; then + echo "$BEGIN_CERT" >"$CA_CERT_PATH" + _get "$Le_LinkIssuer" | _base64 "multiline" >>"$CA_CERT_PATH" + echo "$END_CERT" >>"$CA_CERT_PATH" + _info "The intermediate CA cert is in $(__green " $CA_CERT_PATH ")" + cat "$CA_CERT_PATH" >>"$CERT_FULLCHAIN_PATH" + _info "And the full chain certs is there: $(__green " $CERT_FULLCHAIN_PATH ")" + fi + + Le_CertCreateTime=$(_time) + _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime" + + Le_CertCreateTimeStr=$(date -u) + _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" + + if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ] || [ "$Le_RenewalDays" -gt "$MAX_RENEW" ]; then + Le_RenewalDays="$MAX_RENEW" + else + _savedomainconf "Le_RenewalDays" "$Le_RenewalDays" + fi + + if [ "$CA_BUNDLE" ]; then + _saveaccountconf CA_BUNDLE "$CA_BUNDLE" + else + _clearaccountconf "CA_BUNDLE" + fi + + if [ "$HTTPS_INSECURE" ]; then + _saveaccountconf HTTPS_INSECURE "$HTTPS_INSECURE" + else + _clearaccountconf "HTTPS_INSECURE" + fi + + if [ "$Le_Listen_V4" ]; then + _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" + _cleardomainconf Le_Listen_V6 + elif [ "$Le_Listen_V6" ]; then + _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" + _cleardomainconf Le_Listen_V4 + fi + + Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) + + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" + + Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) + _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" + + _on_issue_success + + if [ "$Le_RealCertPath$Le_RealKeyPath$Le_RealCACertPath$Le_ReloadCmd$Le_RealFullChainPath" ]; then + _installcert + fi + +} + +#domain [isEcc] +renew() { + + _info "CALLING RENEW PROCEDURE" + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --renew -d domain.com [--ecc]" + return 1 + fi + + _isEcc="$2" + + _initpath "$Le_Domain" "$_isEcc" + + _info "$(__green "Renew: '$Le_Domain'")" + if [ ! -f "$DOMAIN_CONF" ]; then + _info "'$Le_Domain' is not a issued domain, skip." + return 0 + fi + + if [ "$Le_RenewalDays" ]; then + _savedomainconf Le_RenewalDays "$Le_RenewalDays" + fi + + . "$DOMAIN_CONF" + + if [ "$Le_API" ]; then + API="$Le_API" + #reload ca configs + ACCOUNT_KEY_PATH="" + ACCOUNT_JSON_PATH="" + CA_CONF="" + _debug3 "initpath again." + _initpath "$Le_Domain" "$_isEcc" + fi + + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _info "Skip, Next renewal time is: $(__green "$Le_NextRenewTimeStr")" + _info "Add '$(__red '--force')' to force to renew." + return "$RENEW_SKIP" + fi + + IS_RENEW="1" + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" + res="$?" + if [ "$res" != "0" ]; then + return "$res" + fi + + _info "CALLING deploy?" + if [ "$Le_DeployHook" ]; then + + _info "DEPLOY HOOK yes sir" + deploy "$Le_Domain" "$Le_DeployHook" "$Le_Keylength" + res="$?" + fi + + IS_RENEW="" + + return "$res" +} + +#renewAll [stopRenewOnError] +renewAll() { + _initpath + _stopRenewOnError="$1" + _debug "_stopRenewOnError" "$_stopRenewOnError" + _ret="0" + + for di in "${CERT_HOME}"/*.*/; do + _debug di "$di" + if ! [ -d "$di" ]; then + _debug "Not directory, skip: $di" + continue + fi + d=$(basename "$di") + _debug d "$d" + ( + if _endswith "$d" "$ECC_SUFFIX"; then + _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) + d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) + fi + renew "$d" "$_isEcc" + ) + rc="$?" + _debug "Return code: $rc" + if [ "$rc" != "0" ]; then + if [ "$rc" = "$RENEW_SKIP" ]; then + _info "Skipped $d" + elif [ "$_stopRenewOnError" ]; then + _err "Error renew $d, stop now." + return "$rc" + else + _ret="$rc" + _err "Error renew $d, Go ahead to next one." + fi + fi + done + return "$_ret" +} + +#csr webroot +signcsr() { + _csrfile="$1" + _csrW="$2" + if [ -z "$_csrfile" ] || [ -z "$_csrW" ]; then + _usage "Usage: $PROJECT_ENTRY --signcsr --csr mycsr.csr -w /path/to/webroot/a.com/ " + return 1 + fi + + _initpath + + _csrsubj=$(_readSubjectFromCSR "$_csrfile") + if [ "$?" != "0" ]; then + _err "Can not read subject from csr: $_csrfile" + return 1 + fi + _debug _csrsubj "$_csrsubj" + + _csrdomainlist=$(_readSubjectAltNamesFromCSR "$_csrfile") + if [ "$?" != "0" ]; then + _err "Can not read domain list from csr: $_csrfile" + return 1 + fi + _debug "_csrdomainlist" "$_csrdomainlist" + + if [ -z "$_csrsubj" ]; then + _csrsubj="$(_getfield "$_csrdomainlist" 1)" + _debug _csrsubj "$_csrsubj" + _csrdomainlist="$(echo "$_csrdomainlist" | cut -d , -f 2-)" + _debug "_csrdomainlist" "$_csrdomainlist" + fi + + if [ -z "$_csrsubj" ]; then + _err "Can not read subject from csr: $_csrfile" + return 1 + fi + + _csrkeylength=$(_readKeyLengthFromCSR "$_csrfile") + if [ "$?" != "0" ] || [ -z "$_csrkeylength" ]; then + _err "Can not read key length from csr: $_csrfile" + return 1 + fi + + _initpath "$_csrsubj" "$_csrkeylength" + mkdir -p "$DOMAIN_PATH" + + _info "Copy csr to: $CSR_PATH" + cp "$_csrfile" "$CSR_PATH" + + issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" + +} + +showcsr() { + _csrfile="$1" + _csrd="$2" + if [ -z "$_csrfile" ] && [ -z "$_csrd" ]; then + _usage "Usage: $PROJECT_ENTRY --showcsr --csr mycsr.csr" + return 1 + fi + + _initpath + + _csrsubj=$(_readSubjectFromCSR "$_csrfile") + if [ "$?" != "0" ] || [ -z "$_csrsubj" ]; then + _err "Can not read subject from csr: $_csrfile" + return 1 + fi + + _info "Subject=$_csrsubj" + + _csrdomainlist=$(_readSubjectAltNamesFromCSR "$_csrfile") + if [ "$?" != "0" ]; then + _err "Can not read domain list from csr: $_csrfile" + return 1 + fi + _debug "_csrdomainlist" "$_csrdomainlist" + + _info "SubjectAltNames=$_csrdomainlist" + + _csrkeylength=$(_readKeyLengthFromCSR "$_csrfile") + if [ "$?" != "0" ] || [ -z "$_csrkeylength" ]; then + _err "Can not read key length from csr: $_csrfile" + return 1 + fi + _info "KeyLength=$_csrkeylength" +} + +list() { + _raw="$1" + _initpath + + _sep="|" + if [ "$_raw" ]; then + printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Created${_sep}Renew" + for di in "${CERT_HOME}"/*.*/; do + if ! [ -d "$di" ]; then + _debug "Not directory, skip: $di" + continue + fi + d=$(basename "$di") + _debug d "$d" + ( + if _endswith "$d" "$ECC_SUFFIX"; then + _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) + d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) + fi + _initpath "$d" "$_isEcc" + if [ -f "$DOMAIN_CONF" ]; then + . "$DOMAIN_CONF" + printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" + fi + ) + done + else + if _exists column; then + list "raw" | column -t -s "$_sep" + else + list "raw" | tr "$_sep" '\t' + fi + fi + +} + +deploy() { + Le_Domain="$1" + Le_DeployHook="$2" + _isEcc="$3" + if [ -z "$Le_DeployHook" ]; then + _usage "Usage: $PROJECT_ENTRY --deploy -d domain.com --deploy-hook cpanel [--ecc] " + return 1 + fi + + _initpath "$Le_Domain" "$_isEcc" + if [ ! -d "$DOMAIN_PATH" ]; then + _err "Domain is not valid:'$Le_Domain'" + return 1 + fi + + _deployApi="$(_findHook "$Le_Domain" deploy "$Le_DeployHook")" + if [ -z "$_deployApi" ]; then + _err "The deploy hook $Le_DeployHook is not found." + return 1 + fi + _debug _deployApi "$_deployApi" + + _savedomainconf Le_DeployHook "$Le_DeployHook" + + if ! ( + if ! . "$_deployApi"; then + _err "Load file $_deployApi error. Please check your api file and try again." + return 1 + fi + + d_command="${Le_DeployHook}_deploy" + if ! _exists "$d_command"; then + _err "It seems that your api file is not correct, it must have a function named: $d_command" + return 1 + fi + + if ! $d_command "$Le_Domain" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then + _err "Error deploy for domain:$Le_Domain" + _on_issue_err + return 1 + fi + ); then + _err "Deploy error." + return 1 + else + _info "$(__green Success)" + fi + +} + +installcert() { + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --installcert -d domain.com [--ecc] [--certpath cert-file-path] [--keypath key-file-path] [--capath ca-cert-file-path] [ --reloadCmd reloadCmd] [--fullchainpath fullchain-path]" + return 1 + fi + + Le_RealCertPath="$2" + Le_RealKeyPath="$3" + Le_RealCACertPath="$4" + Le_ReloadCmd="$5" + Le_RealFullChainPath="$6" + _isEcc="$7" + + _initpath "$Le_Domain" "$_isEcc" + if [ ! -d "$DOMAIN_PATH" ]; then + _err "Domain is not valid:'$Le_Domain'" + return 1 + fi + + _installcert +} + +_installcert() { + _savedomainconf "Le_RealCertPath" "$Le_RealCertPath" + _savedomainconf "Le_RealCACertPath" "$Le_RealCACertPath" + _savedomainconf "Le_RealKeyPath" "$Le_RealKeyPath" + _savedomainconf "Le_ReloadCmd" "$Le_ReloadCmd" + _savedomainconf "Le_RealFullChainPath" "$Le_RealFullChainPath" + + if [ "$Le_RealCertPath" = "$NO_VALUE" ]; then + Le_RealCertPath="" + fi + if [ "$Le_RealKeyPath" = "$NO_VALUE" ]; then + Le_RealKeyPath="" + fi + if [ "$Le_RealCACertPath" = "$NO_VALUE" ]; then + Le_RealCACertPath="" + fi + if [ "$Le_ReloadCmd" = "$NO_VALUE" ]; then + Le_ReloadCmd="" + fi + if [ "$Le_RealFullChainPath" = "$NO_VALUE" ]; then + Le_RealFullChainPath="" + fi + + if [ "$Le_RealCertPath" ]; then + + _info "Installing cert to:$Le_RealCertPath" + if [ -f "$Le_RealCertPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealCertPath" "$Le_RealCertPath".bak + fi + cat "$CERT_PATH" >"$Le_RealCertPath" + fi + + if [ "$Le_RealCACertPath" ]; then + + _info "Installing CA to:$Le_RealCACertPath" + if [ "$Le_RealCACertPath" = "$Le_RealCertPath" ]; then + echo "" >>"$Le_RealCACertPath" + cat "$CA_CERT_PATH" >>"$Le_RealCACertPath" + else + if [ -f "$Le_RealCACertPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealCACertPath" "$Le_RealCACertPath".bak + fi + cat "$CA_CERT_PATH" >"$Le_RealCACertPath" + fi + fi + + if [ "$Le_RealKeyPath" ]; then + + _info "Installing key to:$Le_RealKeyPath" + if [ -f "$Le_RealKeyPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealKeyPath" "$Le_RealKeyPath".bak + fi + cat "$CERT_KEY_PATH" >"$Le_RealKeyPath" + fi + + if [ "$Le_RealFullChainPath" ]; then + + _info "Installing full chain to:$Le_RealFullChainPath" + if [ -f "$Le_RealFullChainPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealFullChainPath" "$Le_RealFullChainPath".bak + fi + cat "$CERT_FULLCHAIN_PATH" >"$Le_RealFullChainPath" + fi + + if [ "$Le_ReloadCmd" ]; then + + _info "Run Le_ReloadCmd: $Le_ReloadCmd" + if (cd "$DOMAIN_PATH" && eval "$Le_ReloadCmd"); then + _info "$(__green "Reload success")" + else + _err "Reload error for :$Le_Domain" + fi + fi + +} + +installcronjob() { + _initpath + if ! _exists "crontab"; then + _err "crontab doesn't exist, so, we can not install cron jobs." + _err "All your certs will not be renewed automatically." + _err "You must add your own cron job to call '$PROJECT_ENTRY --cron' everyday." + return 1 + fi + + _info "Installing cron job" + if ! crontab -l | grep "$PROJECT_ENTRY --cron"; then + if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ]; then + lesh="\"$LE_WORKING_DIR\"/$PROJECT_ENTRY" + else + _err "Can not install cronjob, $PROJECT_ENTRY not found." + return 1 + fi + if _exists uname && uname -a | grep solaris >/dev/null; then + crontab -l | { + cat + echo "0 0 * * * $lesh --cron --home \"$LE_WORKING_DIR\" > /dev/null" + } | crontab -- + else + crontab -l | { + cat + echo "0 0 * * * $lesh --cron --home \"$LE_WORKING_DIR\" > /dev/null" + } | crontab - + fi + fi + if [ "$?" != "0" ]; then + _err "Install cron job failed. You need to manually renew your certs." + _err "Or you can add cronjob by yourself:" + _err "$lesh --cron --home \"$LE_WORKING_DIR\" > /dev/null" + return 1 + fi +} + +uninstallcronjob() { + if ! _exists "crontab"; then + return + fi + _info "Removing cron job" + cr="$(crontab -l | grep "$PROJECT_ENTRY --cron")" + if [ "$cr" ]; then + if _exists uname && uname -a | grep solaris >/dev/null; then + crontab -l | sed "/$PROJECT_ENTRY --cron/d" | crontab -- + else + crontab -l | sed "/$PROJECT_ENTRY --cron/d" | crontab - + fi + LE_WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 9 | tr -d '"')" + _info LE_WORKING_DIR "$LE_WORKING_DIR" + fi + _initpath + +} + +revoke() { + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --revoke -d domain.com" + return 1 + fi + + _isEcc="$2" + + _initpath "$Le_Domain" "$_isEcc" + if [ ! -f "$DOMAIN_CONF" ]; then + _err "$Le_Domain is not a issued domain, skip." + return 1 + fi + + if [ ! -f "$CERT_PATH" ]; then + _err "Cert for $Le_Domain $CERT_PATH is not found, skip." + return 1 + fi + + cert="$(_getfile "${CERT_PATH}" "${BEGIN_CERT}" "${END_CERT}" | tr -d "\r\n" | _urlencode)" + + if [ -z "$cert" ]; then + _err "Cert for $Le_Domain is empty found, skip." + return 1 + fi + + data="{\"resource\": \"revoke-cert\", \"certificate\": \"$cert\"}" + uri="$API/acme/revoke-cert" + + if [ -f "$CERT_KEY_PATH" ]; then + _info "Try domain key first." + if _send_signed_request "$uri" "$data" "" "$CERT_KEY_PATH"; then + if [ -z "$response" ]; then + _info "Revoke success." + rm -f "$CERT_PATH" + return 0 + else + _err "Revoke error by domain key." + _err "$response" + fi + fi + else + _info "Domain key file doesn't exists." + fi + + _info "Try account key." + + if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then + if [ -z "$response" ]; then + _info "Revoke success." + rm -f "$CERT_PATH" + return 0 + else + _err "Revoke error." + _debug "$response" + fi + fi + return 1 +} + +#domain vtype +_deactivate() { + _d_domain="$1" + _d_type="$2" + _initpath + + _d_i=0 + _d_max_retry=9 + while [ "$_d_i" -lt "$_d_max_retry" ]; do + _info "Deactivate: $_d_domain" + _d_i="$(_math $_d_i + 1)" + + if ! __get_domain_new_authz "$_d_domain"; then + _err "Can not get domain new authz token." + return 1 + fi + + authzUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" + _debug "authzUri" "$authzUri" + + if [ ! -z "$code" ] && [ ! "$code" = '201' ]; then + _err "new-authz error: $response" + return 1 + fi + + entry="$(printf "%s\n" "$response" | _egrep_o '{"type":"[^"]*","status":"valid","uri"[^}]*')" + _debug entry "$entry" + + if [ -z "$entry" ]; then + _info "No more valid entry found." + break + fi + + _vtype="$(printf "%s\n" "$entry" | _egrep_o '"type": *"[^"]*"' | cut -d : -f 2 | tr -d '"')" + _debug _vtype "$_vtype" + _info "Found $_vtype" + + uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d : -f 2,3 | tr -d '"')" + _debug uri "$uri" + + if [ "$_d_type" ] && [ "$_d_type" != "$_vtype" ]; then + _info "Skip $_vtype" + continue + fi + + _info "Deactivate: $_vtype" + + if ! _send_signed_request "$authzUri" "{\"resource\": \"authz\", \"status\":\"deactivated\"}"; then + _err "Can not deactivate $_vtype." + return 1 + fi + + _info "Deactivate: $_vtype success." + + done + _debug "$_d_i" + if [ "$_d_i" -lt "$_d_max_retry" ]; then + _info "Deactivated success!" + else + _err "Deactivate failed." + fi + +} + +deactivate() { + _d_domain_list="$1" + _d_type="$2" + _initpath + _debug _d_domain_list "$_d_domain_list" + if [ -z "$(echo $_d_domain_list | cut -d , -f 1)" ]; then + _usage "Usage: $PROJECT_ENTRY --deactivate -d domain.com [-d domain.com]" + return 1 + fi + for _d_dm in $(echo "$_d_domain_list" | tr ',' ' '); do + if [ -z "$_d_dm" ] || [ "$_d_dm" = "$NO_VALUE" ]; then + continue + fi + if ! _deactivate "$_d_dm" "$_d_type"; then + return 1 + fi + done +} + +# Detect profile file if not specified as environment variable +_detect_profile() { + if [ -n "$PROFILE" -a -f "$PROFILE" ]; then + echo "$PROFILE" + return + fi + + DETECTED_PROFILE='' + SHELLTYPE="$(basename "/$SHELL")" + + if [ "$SHELLTYPE" = "bash" ]; then + if [ -f "$HOME/.bashrc" ]; then + DETECTED_PROFILE="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + DETECTED_PROFILE="$HOME/.bash_profile" + fi + elif [ "$SHELLTYPE" = "zsh" ]; then + DETECTED_PROFILE="$HOME/.zshrc" + fi + + if [ -z "$DETECTED_PROFILE" ]; then + if [ -f "$HOME/.profile" ]; then + DETECTED_PROFILE="$HOME/.profile" + elif [ -f "$HOME/.bashrc" ]; then + DETECTED_PROFILE="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + DETECTED_PROFILE="$HOME/.bash_profile" + elif [ -f "$HOME/.zshrc" ]; then + DETECTED_PROFILE="$HOME/.zshrc" + fi + fi + + if [ ! -z "$DETECTED_PROFILE" ]; then + echo "$DETECTED_PROFILE" + fi +} + +_initconf() { + _initpath + if [ ! -f "$ACCOUNT_CONF_PATH" ]; then + echo "#ACCOUNT_CONF_PATH=xxxx + +#ACCOUNT_EMAIL=aaa@example.com # the account email used to register account. +#ACCOUNT_KEY_PATH=\"/path/to/account.key\" +#CERT_HOME=\"/path/to/cert/home\" + + +#LOG_FILE=\"$DEFAULT_LOG_FILE\" +#LOG_LEVEL=1 + +#AUTO_UPGRADE=\"1\" + +#NO_TIMESTAMP=1 +#OPENSSL_BIN=openssl + +#USER_AGENT=\"$USER_AGENT\" + +#USER_PATH= + + + " >"$ACCOUNT_CONF_PATH" + fi +} + +# nocron +_precheck() { + _nocron="$1" + + if ! _exists "curl" && ! _exists "wget"; then + _err "Please install curl or wget first, we need to access http resources." + return 1 + fi + + if [ -z "$_nocron" ]; then + if ! _exists "crontab"; then + _err "It is recommended to install crontab first. try to install 'cron, crontab, crontabs or vixie-cron'." + _err "We need to set cron job to renew the certs automatically." + _err "Otherwise, your certs will not be able to be renewed automatically." + if [ -z "$FORCE" ]; then + _err "Please add '--force' and try install again to go without crontab." + _err "./$PROJECT_ENTRY --install --force" + return 1 + fi + fi + fi + + if ! _exists "$OPENSSL_BIN"; then + _err "Please install openssl first." + _err "We need openssl to generate keys." + return 1 + fi + + if ! _exists "nc"; then + _err "It is recommended to install nc first, try to install 'nc' or 'netcat'." + _err "We use nc for standalone server if you use standalone mode." + _err "If you don't use standalone mode, just ignore this warning." + fi + + return 0 +} + +_setShebang() { + _file="$1" + _shebang="$2" + if [ -z "$_shebang" ]; then + _usage "Usage: file shebang" + return 1 + fi + cp "$_file" "$_file.tmp" + echo "$_shebang" >"$_file" + sed -n 2,99999p "$_file.tmp" >>"$_file" + rm -f "$_file.tmp" +} + +_installalias() { + _initpath + + _envfile="$LE_WORKING_DIR/$PROJECT_ENTRY.env" + if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then + echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile" + echo "$(cat "$_envfile")" | sed "s|^alias le.*$||" >"$_envfile" + echo "$(cat "$_envfile")" | sed "s|^alias le.sh.*$||" >"$_envfile" + fi + + _setopt "$_envfile" "export LE_WORKING_DIR" "=" "\"$LE_WORKING_DIR\"" + _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY\"" + + _profile="$(_detect_profile)" + if [ "$_profile" ]; then + _debug "Found profile: $_profile" + _info "Installing alias to '$_profile'" + _setopt "$_profile" ". \"$_envfile\"" + _info "OK, Close and reopen your terminal to start using $PROJECT_NAME" + else + _info "No profile is found, you will need to go into $LE_WORKING_DIR to use $PROJECT_NAME" + fi + + #for csh + _cshfile="$LE_WORKING_DIR/$PROJECT_ENTRY.csh" + _csh_profile="$HOME/.cshrc" + if [ -f "$_csh_profile" ]; then + _info "Installing alias to '$_csh_profile'" + _setopt "$_cshfile" "setenv LE_WORKING_DIR" " " "\"$LE_WORKING_DIR\"" + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY\"" + _setopt "$_csh_profile" "source \"$_cshfile\"" + fi + + #for tcsh + _tcsh_profile="$HOME/.tcshrc" + if [ -f "$_tcsh_profile" ]; then + _info "Installing alias to '$_tcsh_profile'" + _setopt "$_cshfile" "setenv LE_WORKING_DIR" " " "\"$LE_WORKING_DIR\"" + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY\"" + _setopt "$_tcsh_profile" "source \"$_cshfile\"" + fi + +} + +# nocron +install() { + + if [ -z "$LE_WORKING_DIR" ]; then + LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" + fi + + _nocron="$1" + if ! _initpath; then + _err "Install failed." + return 1 + fi + if [ "$_nocron" ]; then + _debug "Skip install cron job" + fi + + if ! _precheck "$_nocron"; then + _err "Pre-check failed, can not install." + return 1 + fi + + #convert from le + if [ -d "$HOME/.le" ]; then + for envfile in "le.env" "le.sh.env"; do + if [ -f "$HOME/.le/$envfile" ]; then + if grep "le.sh" "$HOME/.le/$envfile" >/dev/null; then + _upgrading="1" + _info "You are upgrading from le.sh" + _info "Renaming \"$HOME/.le\" to $LE_WORKING_DIR" + mv "$HOME/.le" "$LE_WORKING_DIR" + mv "$LE_WORKING_DIR/$envfile" "$LE_WORKING_DIR/$PROJECT_ENTRY.env" + break + fi + fi + done + fi + + _info "Installing to $LE_WORKING_DIR" + + if ! mkdir -p "$LE_WORKING_DIR"; then + _err "Can not create working dir: $LE_WORKING_DIR" + return 1 + fi + + chmod 700 "$LE_WORKING_DIR" + + cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY" + + if [ "$?" != "0" ]; then + _err "Install failed, can not copy $PROJECT_ENTRY" + return 1 + fi + + _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" + + _installalias + + for subf in $_SUB_FOLDERS; do + if [ -d "$subf" ]; then + mkdir -p "$LE_WORKING_DIR/$subf" + cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/ + fi + done + + if [ ! -f "$ACCOUNT_CONF_PATH" ]; then + _initconf + fi + + if [ "$_DEFAULT_ACCOUNT_CONF_PATH" != "$ACCOUNT_CONF_PATH" ]; then + _setopt "$_DEFAULT_ACCOUNT_CONF_PATH" "ACCOUNT_CONF_PATH" "=" "\"$ACCOUNT_CONF_PATH\"" + fi + + if [ "$_DEFAULT_CERT_HOME" != "$CERT_HOME" ]; then + _saveaccountconf "CERT_HOME" "$CERT_HOME" + fi + + if [ "$_DEFAULT_ACCOUNT_KEY_PATH" != "$ACCOUNT_KEY_PATH" ]; then + _saveaccountconf "ACCOUNT_KEY_PATH" "$ACCOUNT_KEY_PATH" + fi + + if [ -z "$_nocron" ]; then + installcronjob + fi + + if [ -z "$NO_DETECT_SH" ]; then + #Modify shebang + if _exists bash; then + _info "Good, bash is found, so change the shebang to use bash as prefered." + _shebang='#!/usr/bin/env bash' + _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang" + for subf in $_SUB_FOLDERS; do + if [ -d "$LE_WORKING_DIR/$subf" ]; then + for _apifile in "$LE_WORKING_DIR/$subf/"*.sh; do + _setShebang "$_apifile" "$_shebang" + done + fi + done + fi + fi + + _info OK +} + +# nocron +uninstall() { + _nocron="$1" + if [ -z "$_nocron" ]; then + uninstallcronjob + fi + _initpath + + _uninstallalias + + rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" + _info "The keys and certs are in $LE_WORKING_DIR, you can remove them by yourself." + +} + +_uninstallalias() { + _initpath + + _profile="$(_detect_profile)" + if [ "$_profile" ]; then + _info "Uninstalling alias from: '$_profile'" + text="$(cat "$_profile")" + echo "$text" | sed "s|^.*\"$LE_WORKING_DIR/$PROJECT_NAME.env\"$||" >"$_profile" + fi + + _csh_profile="$HOME/.cshrc" + if [ -f "$_csh_profile" ]; then + _info "Uninstalling alias from: '$_csh_profile'" + text="$(cat "$_csh_profile")" + echo "$text" | sed "s|^.*\"$LE_WORKING_DIR/$PROJECT_NAME.csh\"$||" >"$_csh_profile" + fi + + _tcsh_profile="$HOME/.tcshrc" + if [ -f "$_tcsh_profile" ]; then + _info "Uninstalling alias from: '$_csh_profile'" + text="$(cat "$_tcsh_profile")" + echo "$text" | sed "s|^.*\"$LE_WORKING_DIR/$PROJECT_NAME.csh\"$||" >"$_tcsh_profile" + fi + +} + +cron() { + IN_CRON=1 + _initpath + if [ "$AUTO_UPGRADE" = "1" ]; then + export LE_WORKING_DIR + ( + if ! upgrade; then + _err "Cron:Upgrade failed!" + return 1 + fi + ) + . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null + + if [ -t 1 ]; then + __INTERACTIVE="1" + fi + + _info "Auto upgraded to: $VER" + fi + renewAll + _ret="$?" + IN_CRON="" + exit $_ret +} + +version() { + echo "$PROJECT" + echo "v$VER" +} + +showhelp() { + _initpath + version + echo "Usage: $PROJECT_ENTRY command ...[parameters].... +Commands: + --help, -h Show this help message. + --version, -v Show version info. + --install Install $PROJECT_NAME to your system. + --uninstall Uninstall $PROJECT_NAME, and uninstall the cron job. + --upgrade Upgrade $PROJECT_NAME to the latest code from $PROJECT . + --issue Issue a cert. + --signcsr Issue a cert from an existing csr. + --deploy Deploy the cert to your server. + --installcert Install the issued cert to apache/nginx or any other server. + --renew, -r Renew a cert. + --renewAll Renew all the certs. + --revoke Revoke a cert. + --list List all the certs. + --showcsr Show the content of a csr. + --installcronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. + --uninstallcronjob Uninstall the cron job. The 'uninstall' command can do this automatically. + --cron Run cron job to renew all the certs. + --toPkcs Export the certificate and key to a pfx file. + --updateaccount Update account info. + --registeraccount Register account key. + --createAccountKey, -cak Create an account private key, professional use. + --createDomainKey, -cdk Create an domain private key, professional use. + --createCSR, -ccsr Create CSR , professional use. + --deactivate Deactivate the domain authz, professional use. + +Parameters: + --domain, -d domain.tld Specifies a domain, used to issue, renew or revoke etc. + --force, -f Used to force to install or force to renew a cert immediately. + --staging, --test Use staging server, just for test. + --debug Output debug info. + + --webroot, -w /path/to/webroot Specifies the web root folder for web root mode. + --standalone Use standalone mode. + --tls Use standalone tls mode. + --apache Use apache mode. + --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file] Use dns mode or dns api. + --dnssleep [$DEFAULT_DNS_SLEEP] The time in seconds to wait for all the txt records to take effect in dns api mode. Default $DEFAULT_DNS_SLEEP seconds. + + --keylength, -k [2048] Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384. + --accountkeylength, -ak [2048] Specifies the account key length. + --log [/path/to/logfile] Specifies the log file. The default is: \"$DEFAULT_LOG_FILE\" if you don't give a file path here. + --log-level 1|2 Specifies the log level, default is 1. + + These parameters are to install the cert to nginx/apache or anyother server after issue/renew a cert: + + --certpath /path/to/real/cert/file After issue/renew, the cert will be copied to this path. + --keypath /path/to/real/key/file After issue/renew, the key will be copied to this path. + --capath /path/to/real/ca/file After issue/renew, the intermediate cert will be copied to this path. + --fullchainpath /path/to/fullchain/file After issue/renew, the fullchain cert will be copied to this path. + + --reloadcmd \"service nginx reload\" After issue/renew, it's used to reload the server. + + --accountconf Specifies a customized account config file. + --home Specifies the home dir for $PROJECT_NAME . + --certhome Specifies the home dir to save all the certs, only valid for '--install' command. + --useragent Specifies the user agent string. it will be saved for future use too. + --accountemail Specifies the account email for registering, Only valid for the '--install' command. + --accountkey Specifies the account key path, Only valid for the '--install' command. + --days Specifies the days to renew the cert when using '--issue' command. The max value is $MAX_RENEW days. + --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. + --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. + --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. + --listraw Only used for '--list' command, list the certs in raw format. + --stopRenewOnError, -se Only valid for '--renewall' command. Stop if one cert has error in renewal. + --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. + --ca-bundle Specifices the path to the CA certificate bundle to verify api server's certificate. + --nocron Only valid for '--install' command, which means: do not install the default cron job. In this case, the certs will not be renewed automatically. + --ecc Specifies to use the ECC cert. Valid for '--installcert', '--renew', '--revoke', '--toPkcs' and '--createCSR' + --csr Specifies the input csr. + --pre-hook Command to be run before obtaining any certificates. + --post-hook Command to be run after attempting to obtain/renew certificates. No matter the obain/renew is success or failed. + --renew-hook Command to be run once for each successfully renewed certificate. + --deploy-hook The hook file to deploy cert + --ocsp-must-staple, --ocsp Generate ocsp must Staple extension. + --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. + --listen-v4 Force standalone/tls server to listen at ipv4. + --listen-v6 Force standalone/tls server to listen at ipv6. + --openssl-bin Specifies a custom openssl bin location. + " +} + +# nocron +_installOnline() { + _info "Installing from online archive." + _nocron="$1" + if [ ! "$BRANCH" ]; then + BRANCH="master" + fi + + target="$PROJECT/archive/$BRANCH.tar.gz" + _info "Downloading $target" + localname="$BRANCH.tar.gz" + if ! _get "$target" >$localname; then + _err "Download error." + return 1 + fi + ( + _info "Extracting $localname" + tar xzf $localname + + cd "$PROJECT_NAME-$BRANCH" + chmod +x $PROJECT_ENTRY + if ./$PROJECT_ENTRY install "$_nocron"; then + _info "Install success!" + fi + + cd .. + + rm -rf "$PROJECT_NAME-$BRANCH" + rm -f "$localname" + ) +} + +upgrade() { + if ( + _initpath + export LE_WORKING_DIR + cd "$LE_WORKING_DIR" + _installOnline "nocron" + ); then + _info "Upgrade success!" + exit 0 + else + _err "Upgrade failed!" + exit 1 + fi +} + +_processAccountConf() { + if [ "$_useragent" ]; then + _saveaccountconf "USER_AGENT" "$_useragent" + elif [ "$USER_AGENT" ] && [ "$USER_AGENT" != "$DEFAULT_USER_AGENT" ]; then + _saveaccountconf "USER_AGENT" "$USER_AGENT" + fi + + if [ "$_accountemail" ]; then + _saveaccountconf "ACCOUNT_EMAIL" "$_accountemail" + elif [ "$ACCOUNT_EMAIL" ] && [ "$ACCOUNT_EMAIL" != "$DEFAULT_ACCOUNT_EMAIL" ]; then + _saveaccountconf "ACCOUNT_EMAIL" "$ACCOUNT_EMAIL" + fi + + if [ "$_openssl_bin" ]; then + _saveaccountconf "OPENSSL_BIN" "$_openssl_bin" + elif [ "$OPENSSL_BIN" ] && [ "$OPENSSL_BIN" != "$DEFAULT_OPENSSL_BIN" ]; then + _saveaccountconf "OPENSSL_BIN" "$OPENSSL_BIN" + fi + + if [ "$_auto_upgrade" ]; then + _saveaccountconf "AUTO_UPGRADE" "$_auto_upgrade" + elif [ "$AUTO_UPGRADE" ]; then + _saveaccountconf "AUTO_UPGRADE" "$AUTO_UPGRADE" + fi + +} + +_process() { + _CMD="" + _domain="" + _altdomains="$NO_VALUE" + _webroot="" + _keylength="" + _accountkeylength="" + _certpath="" + _keypath="" + _capath="" + _fullchainpath="" + _reloadcmd="" + _password="" + _accountconf="" + _useragent="" + _accountemail="" + _accountkey="" + _certhome="" + _httpport="" + _tlsport="" + _dnssleep="" + _listraw="" + _stopRenewOnError="" + #_insecure="" + _ca_bundle="" + _nocron="" + _ecc="" + _csr="" + _pre_hook="" + _post_hook="" + _renew_hook="" + _deploy_hook="" + _logfile="" + _log="" + _local_address="" + _log_level="" + _auto_upgrade="" + _listen_v4="" + _listen_v6="" + _openssl_bin="" + while [ ${#} -gt 0 ]; do + case "${1}" in + + --help | -h) + showhelp + return + ;; + --version | -v) + version + return + ;; + --install) + _CMD="install" + ;; + --uninstall) + _CMD="uninstall" + ;; + --upgrade) + _CMD="upgrade" + ;; + --issue) + _CMD="issue" + ;; + --deploy) + _CMD="deploy" + ;; + --signcsr) + _CMD="signcsr" + ;; + --showcsr) + _CMD="showcsr" + ;; + --installcert | -i) + _CMD="installcert" + ;; + --renew | -r) + _CMD="renew" + ;; + --renewAll | --renewall) + _CMD="renewAll" + ;; + --revoke) + _CMD="revoke" + ;; + --list) + _CMD="list" + ;; + --installcronjob) + _CMD="installcronjob" + ;; + --uninstallcronjob) + _CMD="uninstallcronjob" + ;; + --cron) + _CMD="cron" + ;; + --toPkcs) + _CMD="toPkcs" + ;; + --createAccountKey | --createaccountkey | -cak) + _CMD="createAccountKey" + ;; + --createDomainKey | --createdomainkey | -cdk) + _CMD="createDomainKey" + ;; + --createCSR | --createcsr | -ccr) + _CMD="createCSR" + ;; + --deactivate) + _CMD="deactivate" + ;; + --updateaccount) + _CMD="updateaccount" + ;; + --registeraccount) + _CMD="registeraccount" + ;; + --domain | -d) + _dvalue="$2" + + if [ "$_dvalue" ]; then + if _startswith "$_dvalue" "-"; then + _err "'$_dvalue' is not a valid domain for parameter '$1'" + return 1 + fi + if _is_idn "$_dvalue" && ! _exists idn; then + _err "It seems that $_dvalue is an IDN( Internationalized Domain Names), please install 'idn' command first." + return 1 + fi + + if [ -z "$_domain" ]; then + _domain="$_dvalue" + else + if [ "$_altdomains" = "$NO_VALUE" ]; then + _altdomains="$_dvalue" + else + _altdomains="$_altdomains,$_dvalue" + fi + fi + fi + + shift + ;; + + --force | -f) + FORCE="1" + ;; + --staging | --test) + STAGE="1" + ;; + --debug) + if [ -z "$2" ] || _startswith "$2" "-"; then + DEBUG="1" + else + DEBUG="$2" + shift + fi + ;; + --webroot | -w) + wvalue="$2" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + shift + ;; + --standalone) + wvalue="$NO_VALUE" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --local-address) + lvalue="$2" + _local_address="$_local_address$lvalue," + shift + ;; + --apache) + wvalue="apache" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --tls) + wvalue="$W_TLS" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --dns) + wvalue="dns" + if ! _startswith "$2" "-"; then + wvalue="$2" + shift + fi + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --dnssleep) + _dnssleep="$2" + Le_DNSSleep="$_dnssleep" + shift + ;; + + --keylength | -k) + _keylength="$2" + shift + ;; + --accountkeylength | -ak) + _accountkeylength="$2" + shift + ;; + + --certpath) + _certpath="$2" + shift + ;; + --keypath) + _keypath="$2" + shift + ;; + --capath) + _capath="$2" + shift + ;; + --fullchainpath) + _fullchainpath="$2" + shift + ;; + --reloadcmd | --reloadCmd) + _reloadcmd="$2" + shift + ;; + --password) + _password="$2" + shift + ;; + --accountconf) + _accountconf="$2" + ACCOUNT_CONF_PATH="$_accountconf" + shift + ;; + --home) + LE_WORKING_DIR="$2" + shift + ;; + --certhome) + _certhome="$2" + CERT_HOME="$_certhome" + shift + ;; + --useragent) + _useragent="$2" + USER_AGENT="$_useragent" + shift + ;; + --accountemail) + _accountemail="$2" + ACCOUNT_EMAIL="$_accountemail" + shift + ;; + --accountkey) + _accountkey="$2" + ACCOUNT_KEY_PATH="$_accountkey" + shift + ;; + --days) + _days="$2" + Le_RenewalDays="$_days" + shift + ;; + --httpport) + _httpport="$2" + Le_HTTPPort="$_httpport" + shift + ;; + --tlsport) + _tlsport="$2" + Le_TLSPort="$_tlsport" + shift + ;; + + --listraw) + _listraw="raw" + ;; + --stopRenewOnError | --stoprenewonerror | -se) + _stopRenewOnError="1" + ;; + --insecure) + #_insecure="1" + HTTPS_INSECURE="1" + ;; + --ca-bundle) + _ca_bundle="$(readlink -f "$2")" + CA_BUNDLE="$_ca_bundle" + shift + ;; + --nocron) + _nocron="1" + ;; + --ecc) + _ecc="isEcc" + ;; + --csr) + _csr="$2" + shift + ;; + --pre-hook) + _pre_hook="$2" + shift + ;; + --post-hook) + _post_hook="$2" + shift + ;; + --renew-hook) + _renew_hook="$2" + shift + ;; + --deploy-hook) + _deploy_hook="$2" + shift + ;; + --ocsp-must-staple | --ocsp) + Le_OCSP_Stable="1" + ;; + --log | --logfile) + _log="1" + _logfile="$2" + if _startswith "$_logfile" '-'; then + _logfile="" + else + shift + fi + LOG_FILE="$_logfile" + if [ -z "$LOG_LEVEL" ]; then + LOG_LEVEL="$DEFAULT_LOG_LEVEL" + fi + ;; + --log-level) + _log_level="$2" + LOG_LEVEL="$_log_level" + shift + ;; + --auto-upgrade) + _auto_upgrade="$2" + if [ -z "$_auto_upgrade" ] || _startswith "$_auto_upgrade" '-'; then + _auto_upgrade="1" + else + shift + fi + AUTO_UPGRADE="$_auto_upgrade" + ;; + --listen-v4) + _listen_v4="1" + Le_Listen_V4="$_listen_v4" + ;; + --listen-v6) + _listen_v6="1" + Le_Listen_V6="$_listen_v6" + ;; + --openssl-bin) + _openssl_bin="$2" + OPENSSL_BIN="$_openssl_bin" + ;; + *) + _err "Unknown parameter : $1" + return 1 + ;; + esac + + shift 1 + done + + if [ "${_CMD}" != "install" ]; then + __initHome + if [ "$_log" ]; then + if [ -z "$_logfile" ]; then + _logfile="$DEFAULT_LOG_FILE" + fi + fi + if [ "$_logfile" ]; then + _saveaccountconf "LOG_FILE" "$_logfile" + LOG_FILE="$_logfile" + fi + + if [ "$_log_level" ]; then + _saveaccountconf "LOG_LEVEL" "$_log_level" + LOG_LEVEL="$_log_level" + fi + + _processAccountConf + fi + + _debug2 LE_WORKING_DIR "$LE_WORKING_DIR" + + if [ "$DEBUG" ]; then + version + fi + + case "${_CMD}" in + install) install "$_nocron" ;; + uninstall) uninstall "$_nocron" ;; + upgrade) upgrade ;; + issue) + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_certpath" "$_keypath" "$_capath" "$_reloadcmd" "$_fullchainpath" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" + ;; + deploy) + deploy "$_domain" "$_deploy_hook" "$_ecc" + ;; + signcsr) + signcsr "$_csr" "$_webroot" + ;; + showcsr) + showcsr "$_csr" "$_domain" + ;; + installcert) + installcert "$_domain" "$_certpath" "$_keypath" "$_capath" "$_reloadcmd" "$_fullchainpath" "$_ecc" + ;; + renew) + renew "$_domain" "$_ecc" + ;; + renewAll) + renewAll "$_stopRenewOnError" + ;; + revoke) + revoke "$_domain" "$_ecc" + ;; + deactivate) + deactivate "$_domain,$_altdomains" + ;; + registeraccount) + registeraccount "$_accountkeylength" + ;; + updateaccount) + updateaccount + ;; + list) + list "$_listraw" + ;; + installcronjob) installcronjob ;; + uninstallcronjob) uninstallcronjob ;; + cron) cron ;; + toPkcs) + toPkcs "$_domain" "$_password" "$_ecc" + ;; + createAccountKey) + createAccountKey "$_accountkeylength" + ;; + createDomainKey) + createDomainKey "$_domain" "$_keylength" + ;; + createCSR) + createCSR "$_domain" "$_altdomains" "$_ecc" + ;; + + *) + _err "Invalid command: $_CMD" + showhelp + return 1 + ;; + esac + _ret="$?" + if [ "$_ret" != "0" ]; then + return $_ret + fi + + if [ "${_CMD}" = "install" ]; then + if [ "$_log" ]; then + if [ -z "$LOG_FILE" ]; then + LOG_FILE="$DEFAULT_LOG_FILE" + fi + _saveaccountconf "LOG_FILE" "$LOG_FILE" + fi + + if [ "$_log_level" ]; then + _saveaccountconf "LOG_LEVEL" "$_log_level" + fi + _processAccountConf + fi + +} + +if [ "$INSTALLONLINE" ]; then + INSTALLONLINE="" + _installOnline $BRANCH + exit +fi + +main() { + [ -z "$1" ] && showhelp && return + if _startswith "$1" '-'; then _process "$@"; else "$@"; fi +} + +main "$@" diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh new file mode 100644 index 000000000000..c5e1c1df10b1 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh @@ -0,0 +1,64 @@ +#!/usr/local/bin/php -f +scheme == "ftp" || $this->scheme == "ftps") { echo "\n upload:{$data_to_send} tofile: {$remote_file}"; @@ -134,9 +118,9 @@ class FTPConnection $remote_dir = substr($remote_dir, 0, strlen($remote_dir)-1); } - $parts = explode('/',$remote_dir); // 2013/06/11/username + $parts = explode('/', $remote_dir); foreach($parts as $part){ - if(!@ftp_chdir($this->connection, $part)){ + if(!@ftp_chdir($this->connection, $part)) { ftp_mkdir($this->connection, $part); ftp_chdir($this->connection, $part); //ftp_chmod($ftpcon, 0777, $part); diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc new file mode 100644 index 000000000000..34b4891d1049 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc @@ -0,0 +1,142 @@ +\n".$command; + // add to cron environment path: /usr/local/bin/ + $env = array(); + $env['path'] = "/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin/"; + $descriptorspec = array( + 0 => array("pipe", "r"), // stdin is a pipe that the child will read from + 1 => array("pipe", "w") // stdout is a pipe that the child will write to + ); + $process = proc_open($command, $descriptorspec, $pipes, null, $env); + if (is_resource($process)) { + echo stream_get_contents($pipes[1]); + fclose($pipes[0]); + fclose($pipes[1]); + $return_value = proc_close($process); + } + return $return_value; + } + +class acme_sh { + + private $accountconfig; + private $path_account; + private $name; + + function __construct($name, $ca) { + $this->name = $name; + $this->init($ca, $name); + } + + function init($ca, $name) { + $conf = "API='{$ca}'\n"; + $cahost = parse_url($ca, PHP_URL_HOST); + $acmeconf = "/tmp/acme/{$name}/"; + $this->acmeconf = $acmeconf; + $this->path_account = "$acmeconf/ca/$cahost"; + safe_mkdir($this->path_account); + $this->accountconfig = "{$this->acmeconf}accountconf.conf"; + file_put_contents("{$this->accountconfig}", $conf); + } + + function generateAccountKey() { + unlink_if_exists("{$this->path_account}/account.key"); + exec("/usr/local/pkg/acme/acme.sh --home {$this->acmeconf} --createAccountKey --accountkeylength 4096 --accountconf {$this->accountconfig}"); + $privateKey = file_get_contents("{$this->path_account}/account.key"); + return $privateKey; + } + + function registeraccount($key) { + file_put_contents("{$this->path_account}/account.key", $key); + exec("/usr/local/pkg/acme/acme.sh --home {$this->acmeconf} --registeraccount --accountconf {$this->accountconfig} 2>&1", $output, $err); + return $err == 0; + } + + function generateDomainKey($domain, $keylength) { + global $a_keylength; + $pathadd = ""; + if ($a_keylength[$keylength]['ecc']) { + $pathadd = "_ecc"; + } + $certpath = "{$this->acmeconf}{$domain}{$pathadd}"; + safe_mkdir($certpath); + + unlink_if_exists("{$certpath}/{$domain}.key"); + logexec("/usr/local/pkg/acme/acme.sh --home {$this->acmeconf} --accountconf {$this->accountconfig} --createDomainKey -d $domain --keylength $keylength"); + $privateKey = file_get_contents("{$certpath}/{$domain}.key"); + return $privateKey; + } + + function signCertificate($accountkey, $certificatepsk, $domainstosign) { + $Le_Domain = $domainstosign[0]; + $certpath = "{$this->acmeconf}{$Le_Domain}/"; + $CERT_KEY_PATH = "{$certpath}{$Le_Domain}.key"; + $CERT_PATH = "{$certpath}{$Le_Domain}.cer"; + $CA_CERT_PATH = "{$certpath}ca.cer"; + $CERT_FULLCHAIN_PATH = "{$certpath}fullchain.cer"; + $reloadcmd = "/usr/local/pkg/acme/acme_command.sh \"importcert\" \"{$this->name}\" \"$Le_Domain\" \"$CERT_KEY_PATH\" \"$CERT_PATH\" \"$CA_CERT_PATH\" \"$CERT_FULLCHAIN_PATH\""; + $reloadfile = "{$this->acmeconf}reloadcmd.sh"; + file_put_contents($reloadfile, $reloadcmd); + chmod($reloadfile, 755); + + $hookcontent_httpapi = <<name}" "\$1" "\$2" "\$3" +} + +pfSenseacme_rm() { + /usr/local/pkg/acme/acme_command.sh "removekey" "{$this->name}" "\$1" "\$2" +} +EOF; + safe_mkdir("{$this->acmeconf}httpapi"); + $hookfile_httpapi = "{$this->acmeconf}httpapi/pfSenseacme.sh"; + file_put_contents($hookfile_httpapi, $hookcontent_httpapi); + chmod($hookfile_httpapi, 755); + + $certpath = "{$this->acmeconf}{$domainstosign[0]}"; + safe_mkdir($certpath); + file_put_contents("{$certpath}/{$domainstosign[0]}.key", $certificatepsk); + $domainstr = ""; + foreach($domainstosign as $domain) { + $domainstr .= " -d {$domain}"; + } + logexec("/usr/local/pkg/acme/acme.sh" + . " --issue {$domainstr}" + . " --home {$this->acmeconf}" + . " --accountconf {$this->accountconfig}" + . " --webroot pfSenseacme" + . " --force" + . " --reloadCmd {$this->acmeconf}reloadcmd.sh" + . " --log-level 1" + . " --log {$this->acmeconf}acme_issuecert.log" + . " > {$this->acmeconf}issue.log 2>&1", $output, $err); + $cer = "{$certpath}/{$domainstosign[0]}.cer"; + if (file_exists($cer)) { + return $cer; + } + return false; + } +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc index ff0b376828b9..d8d2f01883e7 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc @@ -1,31 +1,23 @@ -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. - -source: https://github.com/analogic/lescript -*/ - -namespace Analogic\ACME; - -class Lescript -{ - //public $ca = 'https://acme-v01.api.letsencrypt.org'; - //public $ca = 'https://acme-staging.api.letsencrypt.org'; // testing - public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf'; - public $countryCode = 'CZ'; - public $state = "Czech Republic"; - - private $certificatesDir; - private $webRootDir; - - /** @var \Psr\Log\LoggerInterface */ - private $logger; - private $client; - //private $accountKeyPath; - private $privateKey = ""; - public $callback; - - protected function __construct() - { - } - - public static function createWithCustomEvents($ca, $logger = null) { - $instance = new static(); - $instance->logger = $logger; - //echo "CA: $ca"; - $instance->client = new Client($ca); - return $instance; - } - - public function createStandalone($certificatesDir, $webRootDir, $logger = null) - { - $this->certificatesDir = $certificatesDir; - $this->webRootDir = $webRootDir; - $this->logger = $logger; - $this->client = new Client($this->ca); - $this->accountKeyPath = $certificatesDir.'/_account/private.pem'; - } - - /*public function initAccount() - { - if(!is_file($this->accountKeyPath)) { - - // generate and save new private key for account - // --------------------------------------------- - - $this->log('Starting new account registration'); - $this->generateKey(dirname($this->accountKeyPath)); - $this->postNewReg(); - $this->log('New account certificate registered'); - - } else { - - $this->log('Account already registered. Continuing.'); - - } - }*/ - - public function signDomains(array $domains) - { - $this->log('Starting certificate generation process for domains'); - - $privateAccountKey = $this->readPrivateKey($this->accountKeyPath); - $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)) - ); - - // choose http-01 challange 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 = $this->webRootDir.'/.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);*/ - $this->callback->chalenge_response_put($domain, $challenge['token'], $payload); - // 3. verification process itself - // ------------------------------- - - $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - - $this->log("Token for $domain should be available at $uri"); - - // simple self check - if($payload !== trim(@file_get_contents($uri))) { - throw new \RuntimeException("Please check $uri - token not available"); - } - - $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 - do { - if(empty($result['status']) || $result['status'] == "invalid") { - throw new \RuntimeException("Verification ended with error: ".json_encode($result)); - } - $ended = !($result['status'] === "pending"); - - if(!$ended) { - $this->log("Verification pending, sleeping 1s"); - sleep(1); - } - - $result = $this->client->get($location); - - } while (!$ended); - - $this->log("Verification ended with status: ${result['status']}"); - $this->callback->chalenge_response_cleanup($domain, $challenge['token']); - } - - // requesting certificate - // ---------------------- - /*$domainPath = $this->getDomainPath(reset($domains)); - - // generate private key for domain if not exist - if(!is_dir($domainPath) || !is_file($domainPath.'/private.pem')) { - $this->generateKey($domainPath); - } - - // load domain key - $privateDomainKey = $this->readPrivateKey($domainPath.'/private.pem');*/ - $privateDomainKey = $this->readCertificatePSK(); - - $this->client->getLastLinks(); - - // request certificates creation - $result = $this->signedRequest( - "/acme/new-cert", - array('resource' => 'new-cert', 'csr' => $this->generateCSR($privateDomainKey, $domains)) - ); - 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); - - } else if ($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'); - - $this->log("Calling: storeCertificate"); - $this->callback->storeCertificate($certificates); - /*$this->log("Saving fullchain.pem"); - file_put_contents($domainPath.'/fullchain.pem', implode("\n", $certificates)); - - $this->log("Saving cert.pem"); - file_put_contents($domainPath.'/cert.pem', array_shift($certificates)); - - $this->log("Saving chain.pem"); - file_put_contents($domainPath."/chain.pem", implode("\n", $certificates)); - */ - - $this->log("Done !!§§!"); - } - - private function readCertificatePSK(){ - $psk = $this->callback->getCertificatePSK(); - if(($key = openssl_pkey_get_private($psk)) === FALSE) { - throw new \RuntimeException(openssl_error_string()); - } - return $key; - } - - private function readPrivateKey($path) - { - if (!empty($this->privateKey)) { - //echo " USING PSK from STRING ".$this->privateKey; - if(($key = openssl_pkey_get_private($this->privateKey)) === FALSE) { - throw new \RuntimeException(openssl_error_string()); - } - } else { - if(($key = openssl_pkey_get_private('file://'.$path)) === FALSE) { - throw new \RuntimeException(openssl_error_string()); - } - } - return $key; - } - - private function parsePemFromBody($body) - { - $pem = chunk_split(base64_encode($body), 64, "\n"); - return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; - } - - private function getDomainPath($domain) - { - return $this->certificatesDir.'/'.$domain.'/'; - } - - public function postNewReg() - { - $this->log('Sending registration to letsencrypt server'); - - return $this->signedRequest( - '/acme/new-reg', - array('resource' => 'new-reg', 'agreement' => $this->license) - ); - } - - 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 = 2048 -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" => $this->state, - "C" => $this->countryCode, - "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); - - //file_put_contents($this->getDomainPath($domain)."/last.csr", $csr); - preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); - - return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); - } - - private function generateKey($outputDirectory) - { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_RSA, - "private_key_bits" => 4096, - )); - - if(!openssl_pkey_export($res, $privateKey)) { - throw new \RuntimeException("Key export failed!"); - } - - $details = openssl_pkey_get_details($res); - - if(!is_dir($outputDirectory)) @mkdir($outputDirectory, 0700, true); - if(!is_dir($outputDirectory)) throw new \RuntimeException("Cant't create directory $outputDirectory"); - - file_put_contents($outputDirectory.'/private.pem', $privateKey); - file_put_contents($outputDirectory.'/public.pem', $details['key']); - } - - public function setPrivateKey($psk) { - $this->privateKey = $psk; - - } - private function signedRequest($uri, array $payload) - { - $privateKey = $this->readPrivateKey($this->accountKeyPath); - $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) - { - if($this->logger) { - $this->logger->info($message); - } else { - echo $message."\n"; - } - } -} - -class Client -{ - private $lastCode; - private $lastHeader; - - private $base; - - public function __construct($base) - { - $this->base = $base; - } - - private function curl($method, $url, $data = null) - { - //echo "$method, $url, {$this->base}"; - $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]; - } -} - -class Base64UrlSafeEncoder -{ - public static function encode($input) - { - return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); - } - - public static function decode($input) - { - $remainder = strlen($input) % 4; - if ($remainder) { - $padlen = 4 - $remainder; - $input .= str_repeat('=', $padlen); - } - return base64_decode(strtr($input, '-_', '+/')); - } -} diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php index 48cca8dfd502..19cdfbf548ff 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php @@ -1,31 +1,23 @@
@@ -240,13 +232,13 @@ function array_moveitemsbefore(&$items, $before, $selected) {
addInput(new \Form_StaticText( '', - "" + "" . " Create new account key" )); @@ -414,7 +407,7 @@ function table_domains_listitem_change(tableId, fieldId, rowNr, field) { //
@@ -261,13 +253,13 @@ function array_moveitemsbefore(&$items, $before, $selected) {
addInput(new \Form_Input('name', 'Name', 'text', $pconfig['name'] -))->setHelp(''); +))->setHelp('The name set here will also be used to create or overwrite a certificate that might already exist with this name in the pfSense certificate m.'); $section->addInput(new \Form_Input('desc', 'Description', 'text', $pconfig['desc'])); $activedisable = array(); $activedisable['active'] = "Active"; @@ -330,7 +322,6 @@ function updatevisibility() $activedisable )); $a_accountkeys = &$config['installedpackages']['acme']['accountkeys']['item']; -//$a_frontendmode['http'] = array('name' => "http / https(offloading)", 'shortname' => "http/https"); $section->addInput(new \Form_Select( 'acmeaccount', 'Acme Account', @@ -338,15 +329,26 @@ function updatevisibility() form_name_array($a_accountkeys) )); +$a_accountkeys = &$config['installedpackages']['acme']['accountkeys']['item']; +$section->addInput(new \Form_Select( + 'keylength', + 'Key Size', + $pconfig['keylength'], + form_name_array($a_keylength) +)); + $section->addInput(new \Form_StaticText( 'Domain SAN list', - "List all domain names that should be included in the certificate here". -$domainslist->Draw($a_domains) + "List all domain names that should be included in the certificate here, and how to validate ownership by use of a wenroot or dns challenge
" + . "Examples:
" + . "/usr/local/www/.well-known/acme-challenge/
" + . "/tmp/haproxy_chroot/.well-known/acme-challenge/" + . $domainslist->Draw($a_domains) )); $section->addInput(new \Form_StaticText( 'Actions list', - "Used to restart webserver provesses after certificates have been renewed". + "Used to restart webserver processes this certificate has been renewed
Examples:
/etc/rc.restart_webgui
/usr/local/etc/rc.d/haproxy.sh restart". $actionslist->Draw($a_actions) )); diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_generalsettings.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_generalsettings.php index 905ba5a96d10..45230630c543 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_generalsettings.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_generalsettings.php @@ -1,31 +1,23 @@ Date: Sun, 25 Dec 2016 02:37:33 +0100 Subject: [PATCH 08/14] acme, add dns options/scripts from Neilpang/acme.sh into the pfSense-pkg-acme and provide webgui options for them + small fixes --- security/pfSense-pkg-acme/Makefile | 27 ++ .../files/usr/local/pkg/acme/acme.inc | 161 +++++++++- .../files/usr/local/pkg/acme/acme_command.sh | 2 +- .../files/usr/local/pkg/acme/acme_sh.inc | 19 +- .../files/usr/local/pkg/acme/acme_utils.inc | 40 ++- .../usr/local/pkg/acme/dnsapi/dns_ali.sh | 187 +++++++++++ .../usr/local/pkg/acme/dnsapi/dns_aws.sh | 221 +++++++++++++ .../files/usr/local/pkg/acme/dnsapi/dns_cf.sh | 182 +++++++++++ .../files/usr/local/pkg/acme/dnsapi/dns_cx.sh | 216 +++++++++++++ .../files/usr/local/pkg/acme/dnsapi/dns_dp.sh | 223 +++++++++++++ .../files/usr/local/pkg/acme/dnsapi/dns_gd.sh | 117 +++++++ .../local/pkg/acme/dnsapi/dns_ispconfig.sh | 177 +++++++++++ .../usr/local/pkg/acme/dnsapi/dns_lexicon.sh | 73 +++++ .../usr/local/pkg/acme/dnsapi/dns_lua.sh | 143 +++++++++ .../files/usr/local/pkg/acme/dnsapi/dns_me.sh | 146 +++++++++ .../usr/local/pkg/acme/dnsapi/dns_nsupdate.sh | 58 ++++ .../usr/local/pkg/acme/dnsapi/dns_ovh.sh | 295 ++++++++++++++++++ .../usr/local/pkg/acme/dnsapi/dns_pdns.sh | 184 +++++++++++ .../local/www/acme/acme_certificates_edit.php | 5 +- security/pfSense-pkg-acme/pkg-plist | 16 + 20 files changed, 2457 insertions(+), 35 deletions(-) create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ali.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_aws.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_cf.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_cx.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_dp.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_gd.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ispconfig.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lexicon.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lua.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_me.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_nsupdate.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ovh.sh create mode 100644 security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_pdns.sh diff --git a/security/pfSense-pkg-acme/Makefile b/security/pfSense-pkg-acme/Makefile index e42abcb93604..84bda7d48ece 100644 --- a/security/pfSense-pkg-acme/Makefile +++ b/security/pfSense-pkg-acme/Makefile @@ -26,6 +26,7 @@ do-extract: do-install: ${MKDIR} ${STAGEDIR}${PREFIX}/pkg ${MKDIR} ${STAGEDIR}${PREFIX}/pkg/acme + ${MKDIR} ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi ${MKDIR} ${STAGEDIR}${PREFIX}/www ${MKDIR} ${STAGEDIR}${PREFIX}/www/acme ${MKDIR} ${STAGEDIR}/etc/inc/priv @@ -50,6 +51,32 @@ do-install: ${STAGEDIR}${PREFIX}/pkg/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/pkg_acme_tabs.inc \ ${STAGEDIR}${PREFIX}/pkg/acme + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_ovh.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_pdns.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_ali.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_aws.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_cf.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_cx.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_dp.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_gd.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_ispconfig.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_lexicon.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_lua.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_me.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/acme/dnsapi/dns_nsupdate.sh \ + ${STAGEDIR}${PREFIX}/pkg/acme/dnsapi ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_accountkeys.php \ ${STAGEDIR}${PREFIX}/www/acme ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/acme/acme_accountkeys_edit.php \ diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index ed2ff8b7ce10..d97a60ff08c9 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -73,13 +73,13 @@ $a_acmeserver['letsencrypt-production'] = array('name' => "Let's Encrypt Product global $acme_domain_validation_method; $acme_domain_validation_method = array(); -$acme_domain_validation_method['webroot'] = array(name => "local webroot folder", +$acme_domain_validation_method['webroot'] = array(name => "webroot local folder", 'fields' => array( 'folder' => array('name'=>"folder",'columnheader'=>"RootFolder",'type'=>"textbox",'size'=>"50", 'description' =>"Folder the acme challenge response is written to for example: /usr/local/www/.well-known/acme-challenge/" ) )); -$acme_domain_validation_method['ftpwebroot'] = array(name => "FTP webroot", +$acme_domain_validation_method['webrootftp'] = array(name => "webroot FTP", 'fields' => array( 'ftpserver' => array('name'=>"ftpserver",'columnheader'=>"Server",'type'=>"textbox",'size'=>"50", 'description' =>"Hostname of FTP server to connect to for example: ftps://www.webserver.tld " @@ -95,6 +95,143 @@ $acme_domain_validation_method['ftpwebroot'] = array(name => "FTP webroot", 'description' =>"Folder the acme challenge response is written to for default: /.well-known/acme-challenge/" ) )); +$acme_domain_validation_method['dns_ali'] = array(name => "DNS-Aliyuncs", + 'fields' => array( + 'Ali_Key' => array('name'=>"ali_key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'Ali_Secret' => array('name'=>"ali_secret",'columnheader'=>"Secret",'type'=>"textbox", + 'description' =>"Fill in the API Secret" + ) + )); +$acme_domain_validation_method['dns_aws'] = array(name => "DNS-Amazon Route53", + 'fields' => array( + 'AWS_ACCESS_KEY_ID' => array('name'=>"AWS_ACCESS_KEY_ID",'columnheader'=>"Id",'type'=>"textbox", + 'description' =>"Fill in the API Id" + ), + 'AWS_SECRET_ACCESS_KEY' => array('name'=>"AWS_SECRET_ACCESS_KEY",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ) + )); +$acme_domain_validation_method['dns_cf'] = array(name => "DNS-Cloudflare", + 'fields' => array( + 'CF_Key' => array('name'=>"CF_Key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'CF_Email' => array('name'=>"CF_Email",'columnheader'=>"Email",'type'=>"textbox", + 'description' =>"Fill in the API Emailadress" + ) + )); +$acme_domain_validation_method['dns_cx'] = array(name => "DNS-Cloudxns", + 'fields' => array( + 'CX_Key' => array('name'=>"CX_Key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'CX_Secret' => array('name'=>"CX_Secret",'columnheader'=>"Secret",'type'=>"textbox", + 'description' =>"Fill in the API Secret" + ) + )); +$acme_domain_validation_method['dns_dp'] = array(name => "DNS-Dnspod.cn", + 'fields' => array( + 'DP_Id' => array('name'=>"DP_Id",'columnheader'=>"Id",'type'=>"textbox", + 'description' =>"Fill in the API Id" + ), + 'DP_Key' => array('name'=>"DP_Key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ) + )); +$acme_domain_validation_method['dns_gd'] = array(name => "DNS-Godaddy", + 'fields' => array( + 'GD_Key' => array('name'=>"GD_Key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'GD_Secret' => array('name'=>"GD_Secret",'columnheader'=>"Secret",'type'=>"textbox", + 'description' =>"Fill in the API Secret" + ) + )); +$acme_domain_validation_method['dns_ispconfig'] = array(name => "DNS-ISPConfig", + 'fields' => array( + 'ISPC_User' => array('name'=>"ISPC_User",'columnheader'=>"User",'type'=>"textbox", + 'description' =>"Fill in the remoteUser" + ), + 'ISPC_Password' => array('name'=>"ISPC_Password",'columnheader'=>"Password",'type'=>"textbox", + 'description' =>"Fill in the remotePassword" + ), + 'ISPC_Api' => array('name'=>"ISPC_Api",'columnheader'=>"Api",'type'=>"textbox", + 'description' =>"Fill in the https://ispc.domain.tld:8080/remote/json.php" + ), + 'ISPC_Api_Insecure' => array('name'=>"ISPC_Api_Insecure",'columnheader'=>"Secure",'type'=>"textbox", + 'description' =>"Fill in the Set 1 for insecure and 0 for secure -> difference is whether ssl cert is checked for validity (0) or whether it is just accepted (1)" + ) + )); +/* +You must install python and lexicon before using it. ??? +$acme_domain_validation_method['dns_lexicon'] = array(name => "DNS-Lexicon", + 'fields' => array( + '' => array('name'=>"ftpserver",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + '' => array('name'=>"username",'columnheader'=>"Id",'type'=>"textbox", + 'description' =>"Fill in the API Id" + ) + ));*/ +$acme_domain_validation_method['dns_luadns'] = array(name => "DNS-Luadns", + 'fields' => array( + 'LUA_Key' => array('name'=>"LUA_Key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'LUA_Email' => array('name'=>"LUA_Email",'columnheader'=>"Email",'type'=>"textbox", + 'description' =>"Fill in the API Emailadress" + ) + )); +$acme_domain_validation_method['dns_me'] = array(name => "DNS-DNSMadeEasy", + 'fields' => array( + 'ME_Key' => array('name'=>"ME_Key",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'ME_Secret' => array('name'=>"ME_Secret",'columnheader'=>"Secret",'type'=>"textbox", + 'description' =>"Fill in the API Secret" + ) + )); +/*$acme_domain_validation_method['dns_nsupdate'] = array(name => "DNS-NSupdate", + 'fields' => array( + 'NSUPDATE_SERVER' => array('name'=>"NSUPDATE_SERVER",'columnheader'=>"Key",'type'=>"textbox", + 'description' =>"Fill in the API Key" + ), + 'NSUPDATE_KEY' => array('name'=>"NSUPDATE_KEY",'columnheader'=>"Id",'type'=>"textarea", + 'description' =>"Fill in the API Id" + ) + ));needs a file reference..*/ +$acme_domain_validation_method['dns_ovh'] = array(name => "DNS-ovh / kimsufi / soyoustart / runabove", + 'fields' => array( + 'OVH_AK' => array('name'=>"OVH_AK",'columnheader'=>"Application Key",'type'=>"textbox", + 'description' =>"Fill in the Application Key" + ), + 'OVH_AS' => array('name'=>"OVH_AS",'columnheader'=>"Application Secret",'type'=>"textbox", + 'description' =>"Fill in the Application Secret" + ), + 'OVH_CK' => array('name'=>"OVH_CK",'columnheader'=>"Consumer Key",'type'=>"textbox", + 'description' =>"Fill in the Consumer Key" + ), + 'OVH_END_POINT' => array('name'=>"OVH_END_POINT",'columnheader'=>"Endpoint",'type'=>"textbox", + 'description' =>"Fill in one of: ovh-eu/ovh-ca/kimsufi-eu/kimsufi-ca/soyoustart-eu/soyoustart-ca/runabove-ca" + ) + )); +$acme_domain_validation_method['pdns'] = array(name => "DNS-PowerDNS", + 'fields' => array( + 'PDNS_Url' => array('name'=>"PDNS_Url",'columnheader'=>"URL",'type'=>"textbox", + 'description' =>"Fill in the URL http://ns.example.com:8081" + ), + 'PDNS_ServerId' => array('name'=>"PDNS_ServerId",'columnheader'=>"ServerID",'type'=>"textbox", + 'description' =>"Fill in the ServerId localhost" + ), + 'PDNS_Token' => array('name'=>"PDNS_Token",'columnheader'=>"Token",'type'=>"textbox", + 'description' =>"Fill in the Token 0123456789ABCDEF" + ), + 'PDNS_Ttl' => array('name'=>"PDNS_Ttl",'columnheader'=>"TTL",'type'=>"textbox", + 'description' =>"Fill in the TTL 60" + ) + )); //TODO add more challenge validation types /* $acme_domain_validation_method['http-post'] = array(name => "http-post", @@ -103,12 +240,6 @@ $acme_domain_validation_method['http-post'] = array(name => "http-post", 'description' =>"Url the challenge file is posted to, the webserver there must store and reply to the request when the acme servers perform the request for the file from /.well-known/acme-challenge/" ) )); -$acme_domain_validation_method['dns'] = array(name => "dns", - 'fields' => array( - 'url' => array('name'=>"url",'columnheader'=>"Url",'type'=>"textbox",'size'=>"50", - 'description' =>"Verify domain by adding a txt dns record" - ) - )); */ //TODO add more 'actions' @@ -292,7 +423,7 @@ function & get_certificate($name) { echo "## Its time to renew ##\n"; $timetorenew = true; } else { - echo "Renewal number of days not yet reached."; + echo "Renewal number of days not yet reached.\n"; } } @@ -305,6 +436,12 @@ function & get_certificate($name) { continue; } $domainstosign[] = $domain['name']; + $method = $domain['method']; + $envvariables = array(); + global $acme_domain_validation_method; + foreach($acme_domain_validation_method[$method]['fields'] as $key => $field) { + $envvariables[$key] = $domain["{$method}{$field['name']}"]; + } } echo "account: {$certificate['acmeaccount']} \n"; @@ -317,7 +454,7 @@ function & get_certificate($name) { $url = $a_acmeserver[$acmeserver]['url']; $certificatepsk = getCertificatePSK($url, $certificate, $domainstosign[0]); $acmesh = new acme_sh($certificate['name'], $url); - $acmesh->signCertificate($accountkey, $certificatepsk, $domainstosign); + $acmesh->signCertificate($accountkey, $certificatepsk, $domainstosign, $method, $envvariables); } } @@ -352,7 +489,7 @@ function & get_certificate($name) { } } - function chalenge_response_cleanup($certificatename, $domain, $token) { + function challenge_response_cleanup($certificatename, $domain, $token) { $acmecert = get_certificate($certificatename); foreach($acmecert['a_domainlist']['item'] as $domainitem) { if($domainitem['name'] == $domain){ @@ -363,7 +500,7 @@ function & get_certificate($name) { $tokenfile = $domain_info['webrootfolder'] . "/" . $token; @unlink($tokenfile); } - if ($domain_info['method'] == 'ftpwebroot') { + if ($domain_info['method'] == 'webrootftp') { $tokenfile = $domain_info['ftpwebrootfolder'] . "/" . $token; $this->ftp->deleteFile($tokenfile); } diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh index c5e1c1df10b1..83b3a1030ec9 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh @@ -60,5 +60,5 @@ if ($command == "removekey") { $certificatename = $argv[2]; $domain = $argv[3]; $token = $argv[4]; - chalenge_response_cleanup($certificatename, $domain, $token); + challenge_response_cleanup($certificatename, $domain, $token); } \ No newline at end of file diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc index 34b4891d1049..559108a49836 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc @@ -21,11 +21,15 @@ namespace pfsense_pkg\acme; - function logexec($command, &$out = null) { + function logexec($command, $envvariables = null) { echo "
\n".$command; // add to cron environment path: /usr/local/bin/ $env = array(); $env['path'] = "/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin/"; + if (is_array($envvariables)) { + $env = array_merge($env, $envvariables); + } + print_r($env); $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w") // stdout is a pipe that the child will write to @@ -90,7 +94,14 @@ class acme_sh { return $privateKey; } - function signCertificate($accountkey, $certificatepsk, $domainstosign) { + function signCertificate($accountkey, $certificatepsk, $domainstosign, $api = null, $envvariables = null) { + $cmdparameters = ""; + if ($api && substr($api, 0, 4 ) === "dns_") { + $cmdparameters = " --dns {$api}"; + } else { + $cmdparameters = " --webroot pfSenseacme"; + } + $Le_Domain = $domainstosign[0]; $certpath = "{$this->acmeconf}{$Le_Domain}/"; $CERT_KEY_PATH = "{$certpath}{$Le_Domain}.key"; @@ -127,12 +138,12 @@ EOF; . " --issue {$domainstr}" . " --home {$this->acmeconf}" . " --accountconf {$this->accountconfig}" - . " --webroot pfSenseacme" . " --force" . " --reloadCmd {$this->acmeconf}reloadcmd.sh" + . $cmdparameters . " --log-level 1" . " --log {$this->acmeconf}acme_issuecert.log" - . " > {$this->acmeconf}issue.log 2>&1", $output, $err); + . " > {$this->acmeconf}issue.log 2>&1", $envvariables); $cer = "{$certpath}/{$domainstosign[0]}.cer"; if (file_exists($cer)) { return $cer; diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc index d8d2f01883e7..35f42046cf8b 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc @@ -30,20 +30,22 @@ require_once("config.inc"); if(!function_exists('ifset')){ function ifset(&$var, $default = ''){ return isset($var) ? $var : $default; - }; + } } if(!function_exists('is_arrayset')){ function is_arrayset(&$array, $items){ - if (!isset($array)) + if (!isset($array)) { return false; + } $item = $array; $arg = func_get_args(); for($i = 1; $i < count($arg); $i++) { $itemindex = $arg[$i]; - if (!isset($item[$itemindex]) || !is_array($item[$itemindex])) + if (!isset($item[$itemindex]) || !is_array($item[$itemindex])) { return false; + } $item = $item[$itemindex]; } @@ -56,13 +58,14 @@ function phparray_to_javascriptarray_recursive($nestID, $path, $items, $nodeName $itemName = "item$nestID"; //echo "{$offset}$nodeName = {};\n"; echo "{$offset}$nodeName = Object.create(null);\n"; - if (is_array($items)) + if (is_array($items)) { foreach ($items as $key => $item) { - if (in_array($path.'/'.$key, $includeitems)) + if (in_array($path.'/'.$key, $includeitems)) { $subpath = $path.'/'.$key; - else + } else { $subpath = $path.'/*'; + } if (in_array($subpath, $includeitems) || in_array($path.'/*', $includeitems)) { if (is_array($item)) { $subNodeName = "item$nestID"; @@ -74,6 +77,7 @@ function phparray_to_javascriptarray_recursive($nestID, $path, $items, $nodeName } } } + } } function phparray_to_javascriptarray($items, $javaMapName, $includeitems) { phparray_to_javascriptarray_recursive(1,'',$items, $javaMapName, $includeitems); @@ -95,10 +99,12 @@ function html_select_options($keyvaluelist, $selected="") { function echo_html_select($name, $keyvaluelist, $selected, $listEmptyMessage="", $onchangeEvent="", $style="") { $result = ""; if (count($keyvaluelist)>0){ - if ($onchangeEvent != "") + if ($onchangeEvent != "") { $onchangeEvent = " onchange='$onchangeEvent'"; - if ($style != "") + } + if ($style != "") { $style = " style='$style'"; + } $result .= ""; @@ -108,19 +114,23 @@ function echo_html_select($name, $keyvaluelist, $selected, $listEmptyMessage="", return $result; } -function form_keyvalue_array($hap_array) { +function form_keyvalue_array($item_array) { $result = array(); - foreach($hap_array as $key => $item) { - $result[$key] = $item['name']; + if (is_array($item_array)) { + foreach($item_array as $key => $item) { + $result[$key] = $item['name']; + } } return $result; } -function form_name_array($hap_array) { +function form_name_array($item_array) { $result = array(); - foreach($hap_array as $key => $item) { - $name = $item['name']; - $result[$name] = $name; + if (is_array($item_array)) { + foreach($item_array as $key => $item) { + $name = $item['name']; + $result[$name] = $name; + } } return $result; } diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ali.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ali.sh new file mode 100644 index 000000000000..98c56f878830 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ali.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env sh + +Ali_API="https://alidns.aliyuncs.com/" + +#Ali_Key="LTqIA87hOKdjevsf5" +#Ali_Secret="0p5EYueFNq501xnCPzKNbx6K51qPH2" + +#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ali_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$Ali_Key" ] || [ -z "$Ali_Secret" ]; then + Ali_Key="" + Ali_Secret="" + _err "You don't specify aliyun api key and secret yet." + return 1 + fi + + #save the api key and secret to the account conf file. + _saveaccountconf Ali_Key "$Ali_Key" + _saveaccountconf Ali_Secret "$Ali_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + return 1 + fi + + _debug "Add record" + _add_record_query "$_domain" "$_sub_domain" "$txtvalue" && _ali_rest "Add record" +} + +dns_ali_rm() { + fulldomain=$1 + _clean +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _describe_records_query "$h" + if ! _ali_rest "Get root" "ignore"; then + return 1 + fi + + if _contains "$response" "PageNumber"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_ali_rest() { + signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(_hex "$Ali_Secret&")" | _base64) + signature=$(_ali_urlencode "$signature") + url="$Ali_API?$query&Signature=$signature" + + if ! response="$(_get "$url")"; then + _err "Error <$1>" + return 1 + fi + + if [ -z "$2" ]; then + message="$(printf "%s" "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")" + if [ -n "$message" ]; then + _err "$message" + return 1 + fi + fi + + _debug2 response "$response" + return 0 +} + +_ali_urlencode() { + _str="$1" + _str_len=${#_str} + _u_i=1 + while [ "$_u_i" -le "$_str_len" ]; do + _str_c="$(printf "%s" "$_str" | cut -c "$_u_i")" + case $_str_c in [a-zA-Z0-9.~_-]) + printf "%s" "$_str_c" + ;; + *) + printf "%%%02X" "'$_str_c" + ;; + esac + _u_i="$(_math "$_u_i" + 1)" + done +} + +_ali_nonce() { + #_head_n 1 UPSERT$fulldomainTXT300\"$txtvalue\"" + + if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then + _info "txt record updated success." + return 0 + fi + + return 1 +} + +#fulldomain txtvalue +dns_aws_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _aws_tmpl_xml="DELETE\"$txtvalue\"$fulldomain.TXT300" + + if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then + _info "txt record deleted success." + return 0 + fi + + return 1 + +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=2 + p=1 + + if aws_rest GET "2013-04-01/hostedzone"; then + _debug "response" "$response" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "$h."; then + hostedzone="$(echo "$response" | sed 's//\n&/g' | _egrep_o ".*$h.<.Name>.*<.HostedZone>")" + _debug hostedzone "$hostedzone" + if [ -z "$hostedzone" ]; then + _err "Error, can not get hostedzone." + return 1 + fi + _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o ".*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#method uri qstr data +aws_rest() { + mtd="$1" + ep="$2" + qsr="$3" + data="$4" + + _debug mtd "$mtd" + _debug ep "$ep" + _debug qsr "$qsr" + _debug data "$data" + + CanonicalURI="/$ep" + _debug2 CanonicalURI "$CanonicalURI" + + CanonicalQueryString="$qsr" + _debug2 CanonicalQueryString "$CanonicalQueryString" + + RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")" + _debug2 RequestDate "$RequestDate" + + #RequestDate="20161120T141056Z" ############## + + _H1="x-amz-date: $RequestDate" + + aws_host="$AWS_HOST" + CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n" + _debug2 CanonicalHeaders "$CanonicalHeaders" + + SignedHeaders="host;x-amz-date" + _debug2 SignedHeaders "$SignedHeaders" + + RequestPayload="$data" + _debug2 RequestPayload "$RequestPayload" + + Hash="sha256" + + CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$(printf "%s" "$RequestPayload" | _digest "$Hash" hex)" + _debug2 CanonicalRequest "$CanonicalRequest" + + HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)" + _debug2 HashedCanonicalRequest "$HashedCanonicalRequest" + + Algorithm="AWS4-HMAC-SHA256" + _debug2 Algorithm "$Algorithm" + + RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)" + _debug2 RequestDateOnly "$RequestDateOnly" + + Region="us-east-1" + Service="route53" + + CredentialScope="$RequestDateOnly/$Region/$Service/aws4_request" + _debug2 CredentialScope "$CredentialScope" + + StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest" + + _debug2 StringToSign "$StringToSign" + + kSecret="AWS4$AWS_SECRET_ACCESS_KEY" + + #kSecret="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" ############################ + + _debug2 kSecret "$kSecret" + + kSecretH="$(_hex "$kSecret")" + _debug2 kSecretH "$kSecretH" + + kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)" + _debug2 kDateH "$kDateH" + + kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)" + _debug2 kRegionH "$kRegionH" + + kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)" + _debug2 kServiceH "$kServiceH" + + kSigningH="$(printf "aws4_request%s" | _hmac "$Hash" "$kServiceH" hex)" + _debug2 kSigningH "$kSigningH" + + signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)" + _debug2 signature "$signature" + + Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" + _debug2 Authorization "$Authorization" + + _H3="Authorization: $Authorization" + _debug _H3 "$_H3" + + url="$AWS_URL/$ep" + + if [ "$mtd" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url")" + fi + + _ret="$?" + if [ "$_ret" = "0" ]; then + if _contains "$response" "/dev/null; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) + _debug "record_id" "$record_id" + + _cf_rest PUT "zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}" + if [ "$?" = "0" ]; then + _info "Updated, OK" + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain txtvalue +dns_cf_rm() { + fulldomain=$1 + txtvalue=$2 + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain&content=$txtvalue" + + if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _cf_rest DELETE "zones/$_domain_id/dns_records/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" '"success":true' + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _cf_rest GET "zones?name=$h"; then + return 1 + fi + + if printf "%s" "$response" | grep "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\[{\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_cf_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + _H1="X-Auth-Email: $CF_Email" + _H2="X-Auth-Key: $CF_Key" + _H3="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$CF_Api/$ep" "" "$m")" + else + response="$(_get "$CF_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_cx.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_cx.sh new file mode 100644 index 000000000000..f7f2081201db --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_cx.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env sh + +# Cloudxns.com Domain api +# +#CX_Key="1234" +# +#CX_Secret="sADDsdasdgdsf" + +CX_Api="https://www.cloudxns.net/api2" + +#REST_API +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_cx_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then + CX_Key="" + CX_Secret="" + _err "You don't specify cloudxns.com api key or secret yet." + _err "Please create you key and try again." + return 1 + fi + + REST_API="$CX_Api" + + #save the api key and email to the account conf file. + _saveaccountconf CX_Key "$CX_Key" + _saveaccountconf CX_Secret "$CX_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + existing_records "$_domain" "$_sub_domain" + _debug count "$count" + if [ "$?" != "0" ]; then + _err "Error get existing records." + return 1 + fi + + if [ "$count" = "0" ]; then + add_record "$_domain" "$_sub_domain" "$txtvalue" + else + update_record "$_domain" "$_sub_domain" "$txtvalue" + fi + + if [ "$?" = "0" ]; then + return 0 + fi + return 1 +} + +#fulldomain +dns_cx_rm() { + fulldomain=$1 + REST_API="$CX_Api" + if _get_root "$fulldomain"; then + record_id="" + existing_records "$_domain" "$_sub_domain" + if ! [ "$record_id" = "" ]; then + _rest DELETE "record/$record_id/$_domain_id" "{}" + _info "Deleted record ${fulldomain}" + fi + fi +} + +#usage: root sub +#return if the sub record already exists. +#echos the existing records count. +# '0' means doesn't exist +existing_records() { + _debug "Getting txt records" + root=$1 + sub=$2 + count=0 + if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100"; then + return 1 + fi + + seg=$(printf "%s\n" "$response" | _egrep_o '{[^{]*host":"'"$_sub_domain"'"[^}]*\}') + _debug seg "$seg" + if [ -z "$seg" ]; then + return 0 + fi + + if printf "%s" "$response" | grep '"type":"TXT"' >/dev/null; then + count=1 + record_id=$(printf "%s\n" "$seg" | _egrep_o '"record_id":"[^"]*"' | cut -d : -f 2 | tr -d \" | _head_n 1) + _debug record_id "$record_id" + return 0 + fi + +} + +#add the txt record. +#usage: root sub txtvalue +add_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Adding record" + + if ! _rest POST "record" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then + return 1 + fi + + return 0 +} + +#update the txt record +#Usage: root sub txtvalue +update_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Updating record" + + if _rest PUT "record/$record_id" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + + if ! _rest GET "domain"; then + return 1 + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "$h."; then + seg=$(printf "%s\n" "$response" | _egrep_o '{[^{]*"'"$h"'."[^}]*}') + _debug seg "$seg" + _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") + _debug _domain_id "$_domain_id" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + return 1 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +#Usage: method URI data +_rest() { + m=$1 + ep="$2" + _debug ep "$ep" + url="$REST_API/$ep" + _debug url "$url" + + cdate=$(date -u "+%Y-%m-%d %H:%M:%S UTC") + _debug cdate "$cdate" + + data="$3" + _debug data "$data" + + sec="$CX_Key$url$data$cdate$CX_Secret" + _debug sec "$sec" + hmac=$(printf "%s" "$sec" | _digest md5 hex) + _debug hmac "$hmac" + + _H1="API-KEY: $CX_Key" + _H2="API-REQUEST-DATE: $cdate" + _H3="API-HMAC: $hmac" + _H4="Content-Type: application/json" + + if [ "$data" ]; then + response="$(_post "$data" "$url" "" "$m")" + else + response="$(_get "$url")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + if ! _contains "$response" '"message":"success"'; then + return 1 + fi + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_dp.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_dp.sh new file mode 100644 index 000000000000..06833b4bf105 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_dp.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env sh + +# Dnspod.cn Domain api +# +#DP_Id="1234" +# +#DP_Key="sADDsdasdgdsf" + +REST_API="https://dnsapi.cn" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_dp_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$DP_Id" ] || [ -z "$DP_Key" ]; then + DP_Id="" + DP_Key="" + _err "You don't specify dnspod api key and key id yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf DP_Id "$DP_Id" + _saveaccountconf DP_Key "$DP_Key" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + existing_records "$_domain" "$_sub_domain" + _debug count "$count" + if [ "$?" != "0" ]; then + _err "Error get existing records." + return 1 + fi + + if [ "$count" = "0" ]; then + add_record "$_domain" "$_sub_domain" "$txtvalue" + else + update_record "$_domain" "$_sub_domain" "$txtvalue" + fi +} + +#fulldomain txtvalue +dns_dp_rm() { + fulldomain=$1 + txtvalue=$2 + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then + _err "Record.Lis error." + return 1 + fi + + if _contains "$response" 'No records'; then + _info "Don't need to remove." + return 0 + fi + + record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \") + _debug record_id "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id." + return 1 + fi + + if ! _rest POST "Record.Remove" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then + _err "Record.Remove error." + return 1 + fi + + _contains "$response" "Action completed successful" + +} + +#usage: root sub +#return if the sub record already exists. +#echos the existing records count. +# '0' means doesn't exist +existing_records() { + _debug "Getting txt records" + root=$1 + sub=$2 + + if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&domain_id=$_domain_id&sub_domain=$_sub_domain"; then + return 1 + fi + + if _contains "$response" 'No records'; then + count=0 + return 0 + fi + + if _contains "$response" "Action completed successful"; then + count=$(printf "%s" "$response" | grep 'TXT' | wc -l | tr -d ' ') + record_id=$(printf "%s" "$response" | grep '^' | tail -1 | cut -d '>' -f 2 | cut -d '<' -f 1) + _debug record_id "$record_id" + return 0 + else + _err "get existing records error." + return 1 + fi + + count=0 +} + +#add the txt record. +#usage: root sub txtvalue +add_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Adding record" + + if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认"; then + return 1 + fi + + if _contains "$response" "Action completed successful"; then + + return 0 + fi + + return 1 #error +} + +#update the txt record +#Usage: root sub txtvalue +update_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Updating record" + + if ! _rest POST "Record.Modify" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认&record_id=$record_id"; then + return 1 + fi + + if _contains "$response" "Action completed successful"; then + + return 0 + fi + + return 1 #error +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&domain=$h"; then + return 1 + fi + + if _contains "$response" "Action completed successful"; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") + _debug _domain_id "$_domain_id" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + return 1 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +#Usage: method URI data +_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + url="$REST_API/$ep" + + _debug url "$url" + + if [ "$data" ]; then + _debug2 data "$data" + response="$(_post "$data" "$url" | tr -d '\r')" + else + response="$(_get "$url" | tr -d '\r')" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_gd.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_gd.sh new file mode 100644 index 000000000000..81000561842f --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_gd.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env sh + +#Godaddy domain api +# +#GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#GD_Secret="asdfsdfsfsdfsdfdfsdf" + +GD_Api="https://api.godaddy.com/v1" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_gd_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$GD_Key" ] || [ -z "$GD_Secret" ]; then + GD_Key="" + GD_Secret="" + _err "You don't specify godaddy api key and secret yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf GD_Key "$GD_Key" + _saveaccountconf GD_Secret "$GD_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[{\"data\":\"$txtvalue\"}]"; then + if [ "$response" = "{}" ]; then + _info "Added, sleeping 10 seconds" + sleep 10 + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + _err "$response" + return 1 + fi + fi + _err "Add txt record error." + +} + +#fulldomain +dns_gd_rm() { + fulldomain=$1 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _gd_rest GET "domains/$h"; then + return 1 + fi + + if _contains "$response" '"code":"NOT_FOUND"'; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_gd_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + _H1="Authorization: sso-key $GD_Key:$GD_Secret" + _H2="Content-Type: application/json" + + if [ "$data" ]; then + _debug data "$data" + response="$(_post "$data" "$GD_Api/$ep" "" "$m")" + else + response="$(_get "$GD_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ispconfig.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ispconfig.sh new file mode 100644 index 000000000000..a84d95d7d008 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_ispconfig.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env sh + +# ISPConfig 3.1 API +# User must provide login data and URL to the ISPConfig installation incl. port. The remote user in ISPConfig must have access to: +# - DNS zone Functions +# - DNS txt Functions + +# Report bugs to https://github.com/sjau/acme.sh + +# Values to export: +# export ISPC_User="remoteUser" +# export ISPC_Password="remotePassword" +# export ISPC_Api="https://ispc.domain.tld:8080/remote/json.php" +# export ISPC_Api_Insecure=1 # Set 1 for insecure and 0 for secure -> difference is whether ssl cert is checked for validity (0) or whether it is just accepted (1) + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ispconfig_add() { + fulldomain="${1}" + txtvalue="${2}" + _debug "Calling: dns_ispconfig_add() '${fulldomain}' '${txtvalue}'" + _ISPC_credentials && _ISPC_login && _ISPC_getZoneInfo && _ISPC_addTxt +} + +#Usage: dns_myapi_rm _acme-challenge.www.domain.com +dns_ispconfig_rm() { + fulldomain="${1}" + _debug "Calling: dns_ispconfig_rm() '${fulldomain}'" + _ISPC_credentials && _ISPC_login && _ISPC_rmTxt +} + +#################### Private functions below ################################## + +_ISPC_credentials() { + if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then + ISPC_User="" + ISPC_Password="" + ISPC_Api="" + ISPC_Api_Insecure="" + _err "You haven't specified the ISPConfig Login data, URL and whether you want check the ISPC SSL cert. Please try again." + return 1 + else + _saveaccountconf ISPC_User "${ISPC_User}" + _saveaccountconf ISPC_Password "${ISPC_Password}" + _saveaccountconf ISPC_Api "${ISPC_Api}" + _saveaccountconf ISPC_Api_Insecure "${ISPC_Api_Insecure}" + # Set whether curl should use secure or insecure mode + HTTPS_INSECURE="${ISPC_Api_Insecure}" + fi +} + +_ISPC_login() { + _info "Getting Session ID" + curData="{\"username\":\"${ISPC_User}\",\"password\":\"${ISPC_Password}\",\"client_login\":false}" + curResult="$(_post "${curData}" "${ISPC_Api}?login")" + _debug "Calling _ISPC_login: '${curData}' '${ISPC_Api}?login'" + _debug "Result of _ISPC_login: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + sessionID=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _info "Retrieved Session ID." + _debug "Session ID: '${sessionID}'" + else + _err "Couldn't retrieve the Session ID." + return 1 + fi +} + +_ISPC_getZoneInfo() { + _info "Getting Zoneinfo" + zoneEnd=false + curZone="${fulldomain}" + while [ "${zoneEnd}" = false ]; do + # we can strip the first part of the fulldomain, since it's just the _acme-challenge string + curZone="${curZone#*.}" + # suffix . needed for zone -> domain.tld. + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":{\"origin\":\"${curZone}.\"}}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_zone_get")" + _debug "Calling _ISPC_getZoneInfo: '${curData}' '${ISPC_Api}?login'" + _debug "Result of _ISPC_getZoneInfo: '$curResult'" + if _contains "${curResult}" '"id":"'; then + zoneFound=true + zoneEnd=true + _info "Retrieved zone data." + _debug "Zone data: '${curResult}'" + fi + if [ "${curZone#*.}" != "$curZone" ]; then + _debug2 "$curZone still contains a '.' - so we can check next higher level" + else + zoneEnd=true + _err "Couldn't retrieve zone data." + return 1 + fi + done + if [ "${zoneFound}" ]; then + server_id=$(echo "${curResult}" | _egrep_o "server_id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Server ID: '${server_id}'" + case "${server_id}" in + '' | *[!0-9]*) + _err "Server ID is not numeric." + return 1 + ;; + *) _info "Retrieved Server ID" ;; + esac + zone=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Zone: '${zone}'" + case "${zone}" in + '' | *[!0-9]*) + _err "Zone ID is not numeric." + return 1 + ;; + *) _info "Retrieved Zone ID" ;; + esac + client_id=$(echo "${curResult}" | _egrep_o "sys_userid.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Client ID: '${client_id}'" + case "${client_id}" in + '' | *[!0-9]*) + _err "Client ID is not numeric." + return 1 + ;; + *) _info "Retrieved Client ID." ;; + esac + zoneFound="" + zoneEnd="" + fi +} + +_ISPC_addTxt() { + curSerial="$(date +%s)" + curStamp="$(date +'%F %T')" + params="\"server_id\":\"${server_id}\",\"zone\":\"${zone}\",\"name\":\"${fulldomain}.\",\"type\":\"txt\",\"data\":\"${txtvalue}\",\"aux\":\"0\",\"ttl\":\"3600\",\"active\":\"y\",\"stamp\":\"${curStamp}\",\"serial\":\"${curSerial}\"" + curData="{\"session_id\":\"${sessionID}\",\"client_id\":\"${client_id}\",\"params\":{${params}}}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_add")" + _debug "Calling _ISPC_addTxt: '${curData}' '${ISPC_Api}?dns_txt_add'" + _debug "Result of _ISPC_addTxt: '$curResult'" + record_id=$(echo "${curResult}" | _egrep_o "\"response.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Record ID: '${record_id}'" + case "${record_id}" in + '' | *[!0-9]*) + _err "Couldn't add ACME Challenge TXT record to zone." + return 1 + ;; + *) _info "Added ACME Challenge TXT record to zone." ;; + esac +} + +_ISPC_rmTxt() { + # Need to get the record ID. + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":{\"name\":\"${fulldomain}.\",\"type\":\"TXT\"}}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_get")" + _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_get'" + _debug "Result of _ISPC_rmTxt: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + record_id=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Record ID: '${record_id}'" + case "${record_id}" in + '' | *[!0-9]*) + _err "Record ID is not numeric." + return 1 + ;; + *) + unset IFS + _info "Retrieved Record ID." + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\"}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_delete")" + _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_delete'" + _debug "Result of _ISPC_rmTxt: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + _info "Removed ACME Challenge TXT record from zone." + else + _err "Couldn't remove ACME Challenge TXT record from zone." + return 1 + fi + ;; + esac + fi +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lexicon.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lexicon.sh new file mode 100644 index 000000000000..4ab65645c857 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lexicon.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env sh + +# dns api wrapper of lexicon for acme.sh + +lexicon_url="https://github.com/AnalogJ/lexicon" +lexicon_cmd="lexicon" + +wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_lexicon_add() { + fulldomain=$1 + txtvalue=$2 + + domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999) + + if ! _exists "$lexicon_cmd"; then + _err "Please install $lexicon_cmd first: $wiki" + return 1 + fi + + if [ -z "$PROVIDER" ]; then + PROVIDER="" + _err "Please define env PROVIDER first: $wiki" + return 1 + fi + + _savedomainconf PROVIDER "$PROVIDER" + export PROVIDER + + Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr '[a-z]' '[A-Z]') + Lx_name_v=$(eval echo \$"$Lx_name") + _debug "$Lx_name" "$Lx_name_v" + if [ "$Lx_name_v" ]; then + _saveaccountconf "$Lx_name" "$Lx_name_v" + eval export "$Lx_name" + fi + + Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr '[a-z]' '[A-Z]') + Lx_token_v=$(eval echo \$"$Lx_token") + _debug "$Lx_token" "$Lx_token_v" + if [ "$Lx_token_v" ]; then + _saveaccountconf "$Lx_token" "$Lx_token_v" + eval export "$Lx_token" + fi + + Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr '[a-z]' '[A-Z]') + Lx_password_v=$(eval echo \$"$Lx_password") + _debug "$Lx_password" "$Lx_password_v" + if [ "$Lx_password_v" ]; then + _saveaccountconf "$Lx_password" "$Lx_password_v" + eval export "$Lx_password" + fi + + Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr '[a-z]' '[A-Z]') + Lx_domaintoken_v=$(eval echo \$"$Lx_domaintoken") + _debug "$Lx_domaintoken" "$Lx_domaintoken_v" + if [ "$Lx_domaintoken_v" ]; then + eval export "$Lx_domaintoken" + _saveaccountconf "$Lx_domaintoken" "$Lx_domaintoken_v" + fi + + $lexicon_cmd "$PROVIDER" create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" + +} + +#fulldomain +dns_lexicon_rm() { + fulldomain=$1 + +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lua.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lua.sh new file mode 100644 index 000000000000..bc06b3ef17fd --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_lua.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env sh + +# bug reports to dev@1e.ca + +# +#LUA_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#LUA_Email="user@luadns.net" + +LUA_Api="https://api.luadns.com/v1" +LUA_auth=$(printf "%s" "$LUA_Email:$LUA_Key" | _base64) + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_lua_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$LUA_Key" ] || [ -z "$LUA_Email" ]; then + LUA_Key="" + LUA_Email="" + _err "You don't specify luadns api key and email yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf LUA_Key "$LUA_Key" + _saveaccountconf LUA_Email "$LUA_Email" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _LUA_rest GET "zones/${_domain_id}/records" + + if ! _contains "$response" "\"id\":"; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain\"" | wc -l) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + if _LUA_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then + _info "Added" + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | cut -d: -f2 | cut -d, -f1) + _debug "record_id" "$record_id" + + _LUA_rest PUT "zones/$_domain_id/records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"ttl\":120}" + if [ "$?" = "0" ]; then + _info "Updated!" + #todo: check if the record takes effect + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain +dns_lua_rm() { + fulldomain=$1 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + if ! _LUA_rest GET "zones"; then + return 1 + fi + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$h\"" | cut -d : -f 2 | cut -d , -f 1) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_LUA_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + _H1="Accept: application/json" + _H2="Authorization: Basic $LUA_auth" + if [ "$data" ]; then + _debug data "$data" + response="$(_post "$data" "$LUA_Api/$ep" "" "$m")" + else + response="$(_get "$LUA_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_me.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_me.sh new file mode 100644 index 000000000000..d7a1b19f0048 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_me.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env sh + +# bug reports to dev@1e.ca + +# ME_Key=qmlkdjflmkqdjf +# ME_Secret=qmsdlkqmlksdvnnpae + +ME_Api=https://api.dnsmadeeasy.com/V2.0/dns/managed + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_me_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$ME_Key" ] || [ -z "$ME_Secret" ]; then + ME_Key="" + ME_Secret="" + _err "You didn't specify DNSMadeEasy api key and secret yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf ME_Key "$ME_Key" + _saveaccountconf ME_Secret "$ME_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _me_rest GET "${_domain_id}/records?recordName=$_sub_domain&type=TXT" + + if ! _contains "$response" "\"totalRecords\":"; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"totalRecords\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + if _me_rest POST "$_domain_id/records/" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep \"id\": >/dev/null; then + _info "Added" + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | cut -d : -f 2 | head -n 1) + _debug "record_id" "$record_id" + + _me_rest PUT "$_domain_id/records/$record_id/" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}" + if [ "$?" = "0" ]; then + _info "Updated" + #todo: check if the record takes effect + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain +dns_me_rm() { + fulldomain=$1 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _me_rest GET "name?domainname=$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | head -n 1 | cut -d : -f 2) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_me_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + cdate=$(date -u +"%a, %d %b %Y %T %Z") + hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(_hex "$ME_Secret")" hex) + + _H1="x-dnsme-apiKey: $ME_Key" + _H2="x-dnsme-requestDate: $cdate" + _H3="x-dnsme-hmac: $hmac" + + if [ "$data" ]; then + _debug data "$data" + response="$(_post "$data" "$ME_Api/$ep" "" "$m")" + else + response="$(_get "$ME_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_nsupdate.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_nsupdate.sh new file mode 100644 index 000000000000..7acb2ef77de5 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_nsupdate.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env sh + +######## Public functions ##################### + +#Usage: dns_nsupdate_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nsupdate_add() { + fulldomain=$1 + txtvalue=$2 + _checkKeyFile || return 1 + [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" + # save the dns server and key to the account conf file. + _saveaccountconf NSUPDATE_SERVER "${NSUPDATE_SERVER}" + _saveaccountconf NSUPDATE_KEY "${NSUPDATE_KEY}" + _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" + nsupdate -k "${NSUPDATE_KEY}" </dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_ovh_timestamp() { + _H1="" + _H2="" + _H3="" + _H4="" + _H5="" + _get "$OVH_API/auth/time" "" 30 +} + +_ovh_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + _ovh_url="$OVH_API/$ep" + _debug2 _ovh_url "$_ovh_url" + _ovh_t="$(_ovh_timestamp)" + _debug2 _ovh_t "$_ovh_t" + _ovh_p="$OVH_AS+$OVH_CK+$m+$_ovh_url+$data+$_ovh_t" + _debug _ovh_p "$_ovh_p" + _ovh_hex="$(printf "%s" "$_ovh_p" | _digest sha1 hex)" + _debug2 _ovh_hex "$_ovh_hex" + + _H1="X-Ovh-Application: $OVH_AK" + _H2="X-Ovh-Signature: \$1\$$_ovh_hex" + _debug2 _H2 "$_H2" + _H3="X-Ovh-Timestamp: $_ovh_t" + _H4="X-Ovh-Consumer: $OVH_CK" + _H5="Content-Type: application/json;charset=utf-8" + if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ]; then + _debug data "$data" + response="$(_post "$data" "$_ovh_url" "" "$m")" + else + response="$(_get "$_ovh_url")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_pdns.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_pdns.sh new file mode 100644 index 000000000000..06763d8803d6 --- /dev/null +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/dnsapi/dns_pdns.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env sh + +#PowerDNS Emdedded API +#https://doc.powerdns.com/md/httpapi/api_spec/ +# +#PDNS_Url="http://ns.example.com:8081" +#PDNS_ServerId="localhost" +#PDNS_Token="0123456789ABCDEF" +#PDNS_Ttl=60 + +DEFAULT_PDNS_TTL=60 + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "123456789ABCDEF0000000000000000000000000000000000000" +#fulldomain +#txtvalue +dns_pdns_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$PDNS_Url" ]; then + PDNS_Url="" + _err "You don't specify PowerDNS address." + _err "Please set PDNS_Url and try again." + return 1 + fi + + if [ -z "$PDNS_ServerId" ]; then + PDNS_ServerId="" + _err "You don't specify PowerDNS server id." + _err "Please set you PDNS_ServerId and try again." + return 1 + fi + + if [ -z "$PDNS_Token" ]; then + PDNS_Token="" + _err "You don't specify PowerDNS token." + _err "Please create you PDNS_Token and try again." + return 1 + fi + + if [ -z "$PDNS_Ttl" ]; then + PDNS_Ttl="$DEFAULT_PDNS_TTL" + fi + + #save the api addr and key to the account conf file. + _saveaccountconf PDNS_Url "$PDNS_Url" + _saveaccountconf PDNS_ServerId "$PDNS_ServerId" + _saveaccountconf PDNS_Token "$PDNS_Token" + + if [ "$PDNS_Ttl" != "$DEFAULT_PDNS_TTL" ]; then + _saveaccountconf PDNS_Ttl "$PDNS_Ttl" + fi + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain "$_domain" + + if ! set_record "$_domain" "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +#fulldomain +dns_pdns_rm() { + fulldomain=$1 + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain "$_domain" + + if ! rm_record "$_domain" "$fulldomain"; then + return 1 + fi + + return 0 +} + +set_record() { + _info "Adding record" + root=$1 + full=$2 + txtvalue=$3 + + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [{\"name\": \"$full.\", \"type\": \"TXT\", \"content\": \"\\\"$txtvalue\\\"\", \"disabled\": false, \"ttl\": $PDNS_Ttl}]}]}"; then + _err "Set txt record error." + return 1 + fi + + if ! notify_slaves "$root"; then + return 1 + fi + + return 0 +} + +rm_record() { + _info "Remove record" + root=$1 + full=$2 + + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then + _err "Delete txt record error." + return 1 + fi + + if ! notify_slaves "$root"; then + return 1 + fi + + return 0 +} + +notify_slaves() { + root=$1 + + if ! _pdns_rest "PUT" "/api/v1/servers/$PDNS_ServerId/zones/$root./notify"; then + _err "Notify slaves error." + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + + if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then + _zones_response="$response" + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + return 1 + fi + + if _contains "$_zones_response" "\"name\": \"$h.\""; then + _domain="$h" + return 0 + fi + + i=$(_math $i + 1) + done + _debug "$domain not found" + + return 1 +} + +_pdns_rest() { + method=$1 + ep=$2 + data=$3 + + _H1="X-API-Key: $PDNS_Token" + + if [ ! "$method" = "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$PDNS_Url$ep" "" "$method")" + else + response="$(_get "$PDNS_Url$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + + return 0 +} diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php index 4380efc8990e..de5dde93505c 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php @@ -145,11 +145,10 @@ function customdrawcell_actions($object, $item, $itemvalue, $editable, $itemname echo $itemvalue; } } - if (isset($id) && $a_certificates[$id]) { $a_domains = &$a_certificates[$id]['a_domainlist']['item']; $a_actions = &$a_certificates[$id]['a_actions']['item']; - + $pconfig["lastrenewal"] = $a_certificates[$id]["lastrenewal"]; foreach($simplefields as $stat) { $pconfig[$stat] = $a_certificates[$id][$stat]; @@ -339,7 +338,7 @@ function updatevisibility() $section->addInput(new \Form_StaticText( 'Domain SAN list', - "List all domain names that should be included in the certificate here, and how to validate ownership by use of a wenroot or dns challenge
" + "List all domain names that should be included in the certificate here, and how to validate ownership by use of a webroot or dns challenge
" . "Examples:
" . "/usr/local/www/.well-known/acme-challenge/
" . "/tmp/haproxy_chroot/.well-known/acme-challenge/" diff --git a/security/pfSense-pkg-acme/pkg-plist b/security/pfSense-pkg-acme/pkg-plist index 6de5ccebb744..dd05c22b09f2 100644 --- a/security/pfSense-pkg-acme/pkg-plist +++ b/security/pfSense-pkg-acme/pkg-plist @@ -9,6 +9,19 @@ pkg/acme/acme.sh pkg/acme/acme_command.sh pkg/acme/acme_sh.inc pkg/acme/pkg_acme_tabs.inc +pkg/acme/dnsapi/dns_ovh.sh +pkg/acme/dnsapi/dns_pdns.sh +pkg/acme/dnsapi/dns_ali.sh +pkg/acme/dnsapi/dns_aws.sh +pkg/acme/dnsapi/dns_cf.sh +pkg/acme/dnsapi/dns_cx.sh +pkg/acme/dnsapi/dns_dp.sh +pkg/acme/dnsapi/dns_gd.sh +pkg/acme/dnsapi/dns_ispconfig.sh +pkg/acme/dnsapi/dns_lexicon.sh +pkg/acme/dnsapi/dns_lua.sh +pkg/acme/dnsapi/dns_me.sh +pkg/acme/dnsapi/dns_nsupdate.sh www/acme/acme_accountkeys.php www/acme/acme_accountkeys_edit.php www/acme/acme_certificates.php @@ -17,3 +30,6 @@ www/acme/acme_generalsettings.php /etc/inc/priv/acme.priv.inc %%DATADIR%%/info.xml @dir /etc/inc/priv + + + From f0aa9c01dcb5d8c24e72b4282b17c89597de85f3 Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Mon, 26 Dec 2016 22:01:41 +0100 Subject: [PATCH 09/14] acme, dns-manual option, and fixes for enable/disable setting and writing the proper accountkey --- .../files/usr/local/pkg/acme/acme.inc | 33 +++++--- .../files/usr/local/pkg/acme/acme_command.sh | 32 +++---- .../files/usr/local/pkg/acme/acme_sh.inc | 81 +++++++++++++----- .../files/usr/local/pkg/acme/acme_utils.inc | 34 ++++++++ .../usr/local/www/acme/acme_accountkeys.php | 55 ------------ .../local/www/acme/acme_accountkeys_edit.php | 2 +- .../usr/local/www/acme/acme_certificates.php | 83 ++++++++++--------- .../local/www/acme/acme_certificates_edit.php | 2 +- 8 files changed, 176 insertions(+), 146 deletions(-) diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index d97a60ff08c9..f261e44388cc 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -95,6 +95,9 @@ $acme_domain_validation_method['webrootftp'] = array(name => "webroot FTP", 'description' =>"Folder the acme challenge response is written to for default: /.well-known/acme-challenge/" ) )); +$acme_domain_validation_method['dns_manual'] = array(name => "DNS-Manual", + 'fields' => array( + )); $acme_domain_validation_method['dns_ali'] = array(name => "DNS-Aliyuncs", 'fields' => array( 'Ali_Key' => array('name'=>"ali_key",'columnheader'=>"Key",'type'=>"textbox", @@ -361,7 +364,7 @@ function & get_certificate($name) { if (is_array($a_global['certificates']['item'])) { foreach($a_global['certificates']['item'] as $certificate) { echo "Checking if renewal is needed for: {$certificate['name']}\n"; - renew_certificate($certificate['name']); + issue_certificate($certificate['name']); } } } @@ -405,7 +408,7 @@ function & get_certificate($name) { return false; } - function renew_certificate($id, $force = false) { + function issue_certificate($id, $force = false, $renew = false) { $certificate = & get_certificate($id); if (!$force) { if ($certificate['status'] != 'active') { @@ -431,16 +434,19 @@ function & get_certificate($name) { syslog(LOG_NOTICE, "Acme, renewing certificate: {$id}"); echo "Renewing certificate"; $domainstosign = array(); - foreach($certificate['a_domainlist']['item'] as $domain) { - if ($domain['status'] == 'disable') { - continue; - } - $domainstosign[] = $domain['name']; - $method = $domain['method']; - $envvariables = array(); - global $acme_domain_validation_method; - foreach($acme_domain_validation_method[$method]['fields'] as $key => $field) { - $envvariables[$key] = $domain["{$method}{$field['name']}"]; + $method = ""; + if (is_array($certificate['a_domainlist']['item'])) { + foreach($certificate['a_domainlist']['item'] as $domain) { + if ($domain['status'] == 'disable') { + continue; + } + $domainstosign[] = $domain['name']; + $method = $domain['method']; + $envvariables = array(); + global $acme_domain_validation_method; + foreach($acme_domain_validation_method[$method]['fields'] as $key => $field) { + $envvariables[$key] = $domain["{$method}{$field['name']}"]; + } } } @@ -454,7 +460,8 @@ function & get_certificate($name) { $url = $a_acmeserver[$acmeserver]['url']; $certificatepsk = getCertificatePSK($url, $certificate, $domainstosign[0]); $acmesh = new acme_sh($certificate['name'], $url); - $acmesh->signCertificate($accountkey, $certificatepsk, $domainstosign, $method, $envvariables); + $action = $renew == true ? "renew" : "issue"; + $acmesh->signCertificate($action, $accountkey, $certificatepsk, $domainstosign, $method, $envvariables); } } diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh index 83b3a1030ec9..017f5ded4cc3 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh @@ -29,21 +29,23 @@ if ($command == "importcert") { $changedesc .= "Storing signed certificate: " . $certificatename; write_config($changedesc); - foreach($certificate['a_actionlist']['item'] as $action) { - if ($action['status'] == "disable") { - continue; - } - if ($action['method'] == "shellcommand") { - syslog(LOG_NOTICE, "Acme, Running {$action['command']}"); - mwexec_bg($action['command']); - } - if ($action['method'] == "php_command") { - syslog(LOG_NOTICE, "Acme, Running php {$action['command']}"); - eval($action['command']); - } - if ($action['method'] == "servicerestart") { - syslog(LOG_NOTICE, "Acme, Restarting service {$action['command']}"); - restart_service($action['command']); + if (is_array($certificate['a_actionlist']['item'])) { + foreach($certificate['a_actionlist']['item'] as $action) { + if ($action['status'] == "disable") { + continue; + } + if ($action['method'] == "shellcommand") { + syslog(LOG_NOTICE, "Acme, Running {$action['command']}"); + mwexec_bg($action['command']); + } + if ($action['method'] == "php_command") { + syslog(LOG_NOTICE, "Acme, Running php {$action['command']}"); + eval($action['command']); + } + if ($action['method'] == "servicerestart") { + syslog(LOG_NOTICE, "Acme, Restarting service {$action['command']}"); + restart_service($action['command']); + } } } } diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc index 559108a49836..bfdb46af10d6 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc @@ -20,36 +20,47 @@ */ namespace pfsense_pkg\acme; + +class acme_sh { - function logexec($command, $envvariables = null) { - echo "
\n".$command; + private $accountconfig; + private $path_account; + private $name; + private $debug = true; + + private function execacmesh($command, $envvariables = null) { + $command = "/usr/local/pkg/acme/acme.sh " . $command; + if ($this->debug) { + echo "
\n".$command."
\n"; + } // add to cron environment path: /usr/local/bin/ $env = array(); $env['path'] = "/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin/"; if (is_array($envvariables)) { $env = array_merge($env, $envvariables); } - print_r($env); + if ($this->debug) { + print_r($env); + } $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from - 1 => array("pipe", "w") // stdout is a pipe that the child will write to + 1 => array("pipe", "w"), // stdout is a pipe that the child will write to + 2 => array("pipe", "w") // stderr ); $process = proc_open($command, $descriptorspec, $pipes, null, $env); if (is_resource($process)) { - echo stream_get_contents($pipes[1]); + if ($this->debug) { + echo stream_get_contents($pipes[1]); + echo stream_get_contents($pipes[2]); + } fclose($pipes[0]); fclose($pipes[1]); + fclose($pipes[2]); $return_value = proc_close($process); } return $return_value; } -class acme_sh { - - private $accountconfig; - private $path_account; - private $name; - function __construct($name, $ca) { $this->name = $name; $this->init($ca, $name); @@ -68,15 +79,21 @@ class acme_sh { function generateAccountKey() { unlink_if_exists("{$this->path_account}/account.key"); - exec("/usr/local/pkg/acme/acme.sh --home {$this->acmeconf} --createAccountKey --accountkeylength 4096 --accountconf {$this->accountconfig}"); + $this->debug = false; + $this->execacmesh("--home {$this->acmeconf} --createAccountKey --accountkeylength 4096 --accountconf {$this->accountconfig}"); $privateKey = file_get_contents("{$this->path_account}/account.key"); return $privateKey; } function registeraccount($key) { file_put_contents("{$this->path_account}/account.key", $key); - exec("/usr/local/pkg/acme/acme.sh --home {$this->acmeconf} --registeraccount --accountconf {$this->accountconfig} 2>&1", $output, $err); - return $err == 0; + $result = $this->execacmesh("" + . " --home {$this->acmeconf}" + . " --registeraccount" + . " --accountconf {$this->accountconfig}" + . " --log-level 3" + . " --log {$this->acmeconf}acme_issuecert.log"); + return $result == 0; } function generateDomainKey($domain, $keylength) { @@ -89,14 +106,25 @@ class acme_sh { safe_mkdir($certpath); unlink_if_exists("{$certpath}/{$domain}.key"); - logexec("/usr/local/pkg/acme/acme.sh --home {$this->acmeconf} --accountconf {$this->accountconfig} --createDomainKey -d $domain --keylength $keylength"); + $this->execacmesh("" + . " --home {$this->acmeconf}" + . " --accountconf {$this->accountconfig}" + . " --createDomainKey -d $domain" + . " --keylength $keylength" + . " --log-level 3" + . " --log {$this->acmeconf}acme_createdomainkey.log"); $privateKey = file_get_contents("{$certpath}/{$domain}.key"); return $privateKey; } - function signCertificate($accountkey, $certificatepsk, $domainstosign, $api = null, $envvariables = null) { + function signCertificate($action, $accountkey, $certificatepsk, $domainstosign, $api = null, $envvariables = null) { + // $action = issue / renew + file_put_contents("{$this->path_account}/account.key", base64_decode($accountkey)); $cmdparameters = ""; if ($api && substr($api, 0, 4 ) === "dns_") { + if ($api == "dns_manual") { + $api = ""; + } $cmdparameters = " --dns {$api}"; } else { $cmdparameters = " --webroot pfSenseacme"; @@ -108,10 +136,16 @@ class acme_sh { $CERT_PATH = "{$certpath}{$Le_Domain}.cer"; $CA_CERT_PATH = "{$certpath}ca.cer"; $CERT_FULLCHAIN_PATH = "{$certpath}fullchain.cer"; - $reloadcmd = "/usr/local/pkg/acme/acme_command.sh \"importcert\" \"{$this->name}\" \"$Le_Domain\" \"$CERT_KEY_PATH\" \"$CERT_PATH\" \"$CA_CERT_PATH\" \"$CERT_FULLCHAIN_PATH\""; + $reloadcmd = "/usr/local/pkg/acme/acme_command.sh" + . " importcert \"{$this->name}\"" + . " \"$Le_Domain\"" + . " \"$CERT_KEY_PATH\"" + . " \"$CERT_PATH\"" + . " \"$CA_CERT_PATH\"" + . " \"$CERT_FULLCHAIN_PATH\""; $reloadfile = "{$this->acmeconf}reloadcmd.sh"; file_put_contents($reloadfile, $reloadcmd); - chmod($reloadfile, 755); + chmod($reloadfile, 0755); $hookcontent_httpapi = <<acmeconf}httpapi"); $hookfile_httpapi = "{$this->acmeconf}httpapi/pfSenseacme.sh"; file_put_contents($hookfile_httpapi, $hookcontent_httpapi); - chmod($hookfile_httpapi, 755); + chmod($hookfile_httpapi, 0755); $certpath = "{$this->acmeconf}{$domainstosign[0]}"; safe_mkdir($certpath); @@ -134,16 +168,17 @@ EOF; foreach($domainstosign as $domain) { $domainstr .= " -d {$domain}"; } - logexec("/usr/local/pkg/acme/acme.sh" - . " --issue {$domainstr}" + $this->execacmesh("" + . " --{$action} {$domainstr}" . " --home {$this->acmeconf}" . " --accountconf {$this->accountconfig}" . " --force" . " --reloadCmd {$this->acmeconf}reloadcmd.sh" . $cmdparameters - . " --log-level 1" + . " --log-level 3" . " --log {$this->acmeconf}acme_issuecert.log" - . " > {$this->acmeconf}issue.log 2>&1", $envvariables); + //. " > {$this->acmeconf}issue.log 2>&1" + , $envvariables); $cer = "{$certpath}/{$domainstosign[0]}.cer"; if (file_exists($cer)) { return $cer; diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc index 35f42046cf8b..b9a59443d31f 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_utils.inc @@ -53,6 +53,40 @@ if(!function_exists('is_arrayset')){ } } +function array_moveitemsbefore(&$items, $before, $selected) { + // generic function to move array items before the set item by their numeric indexes. + + $a_new = array(); + /* copy all entries < $before and not selected */ + for ($i = 0; $i < $before; $i++) { + if (!in_array($i, $selected)) { + $a_new[] = $items[$i]; + } + } + /* copy all selected entries */ + for ($i = 0; $i < count($items); $i++) { + if ($i == $before) { + continue; + } + if (in_array($i, $selected)) { + $a_new[] = $items[$i]; + } + } + /* copy $before entry */ + if ($before < count($items)) { + $a_new[] = $items[$before]; + } + /* copy all entries > $before and not selected */ + for ($i = $before+1; $i < count($items); $i++) { + if (!in_array($i, $selected)) { + $a_new[] = $items[$i]; + } + } + if (count($a_new) > 0) { + $items = $a_new; + } +} + function phparray_to_javascriptarray_recursive($nestID, $path, $items, $nodeName, $includeitems) { $offset = str_repeat(' ',$nestID); $itemName = "item$nestID"; diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php index 19cdfbf548ff..c34e7a67017e 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php @@ -36,61 +36,6 @@ } $a_accountkeys = &$config['installedpackages']['acme']['accountkeys']['item']; -function array_moveitemsbefore(&$items, $before, $selected) { - // generic function to move array items before the set item by their numeric indexes. - - $a_new = array(); - /* copy all entries < $before and not selected */ - for ($i = 0; $i < $before; $i++) { - if (!in_array($i, $selected)) { - $a_new[] = $items[$i]; - } - } - /* copy all selected entries */ - for ($i = 0; $i < count($items); $i++) { - if ($i == $before) { - continue; - } - if (in_array($i, $selected)) { - $a_new[] = $items[$i]; - } - } - /* copy $before entry */ - if ($before < count($items)) { - $a_new[] = $items[$before]; - } - /* copy all entries > $before and not selected */ - for ($i = $before+1; $i < count($items); $i++) { - if (!in_array($i, $selected)) { - $a_new[] = $items[$i]; - } - } - if (count($a_new) > 0) { - $items = $a_new; - } -} - -if($_POST['action'] == "toggle") { - $id = $_POST['id']; - echo "$id|"; - if (isset($a_certifcates[get_certificate_id($id)])) { - $item = &$a_certifcates[get_certificate_id($id)]; - if ($item['status'] != "disabled"){ - $item['status'] = 'disabled'; - echo "0|"; - }else{ - $item['status'] = 'active'; - echo "1|"; - } - $changedesc .= " set item '$id' status to: {$item['status']}"; - - touch($d_acmeconfdirty_path); - write_config($changedesc); - } - echo "ok|"; - exit; -} - if ($_POST) { $pconfig = $_POST; diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys_edit.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys_edit.php index 31abb1b797ef..df58e0562c64 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys_edit.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys_edit.php @@ -423,7 +423,7 @@ function createkey() { events.push(function() { $('#btnregisterkey').click(function() { $("#btnregisterkeyicon").removeClass("fa-check").addClass("fa-cog fa-spin"); - var key = $("#accountkey").text(); + var key = $("#accountkey").val(); var caname = $("#acmeserver").val(); ajaxRequest = $.ajax({ type: "post", diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php index cbe9bb2f265a..2280625217ff 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates.php @@ -36,40 +36,6 @@ } $a_certifcates = &$config['installedpackages']['acme']['certificates']['item']; -function array_moveitemsbefore(&$items, $before, $selected) { - // generic function to move array items before the set item by their numeric indexes. - - $a_new = array(); - /* copy all entries < $before and not selected */ - for ($i = 0; $i < $before; $i++) { - if (!in_array($i, $selected)) { - $a_new[] = $items[$i]; - } - } - /* copy all selected entries */ - for ($i = 0; $i < count($items); $i++) { - if ($i == $before) { - continue; - } - if (in_array($i, $selected)) { - $a_new[] = $items[$i]; - } - } - /* copy $before entry */ - if ($before < count($items)) { - $a_new[] = $items[$before]; - } - /* copy all entries > $before and not selected */ - for ($i = $before+1; $i < count($items); $i++) { - if (!in_array($i, $selected)) { - $a_new[] = $items[$i]; - } - } - if (count($a_new) > 0) { - $items = $a_new; - } -} - if($_POST['action'] == "toggle") { $id = $_POST['id']; echo "$id|"; @@ -90,11 +56,19 @@ function array_moveitemsbefore(&$items, $before, $selected) { echo "ok|"; exit; } -if($_POST['action'] == "renew") { +if($_POST['action'] == "issuecert") { $id = $_POST['id']; echo $id . "\n"; if (isset($a_certifcates[get_certificate_id($id)])) { - renew_certificate($id, true); + issue_certificate($id, true); + } + exit; +} +if($_POST['action'] == "renewcert") { + $id = $_POST['id']; + echo $id . "\n"; + if (isset($a_certifcates[get_certificate_id($id)])) { + issue_certificate($id, true, true); } exit; } @@ -246,9 +220,29 @@ function array_moveitemsbefore(&$items, $before, $selected) {
- - "> + + "> - + - + - + - - + + - + - + - + - + - +
- + - + - +
+ Renew + + Issue + + + + Issue/Renew + + @@ -314,20 +308,33 @@ function js_callback(req_content) { } } +function issuecertificate($id) { + $('#'+"btnissueicon_"+$id).removeClass("fa-check").addClass("fa-cog fa-spin"); + + ajaxRequest = $.ajax({ + url: "", + type: "post", + data: { id: $id, action: "issuecert"}, + success: function(data) { + js_callbackrenew(data); + $("#btnissueicon_"+$id).removeClass("fa-cog fa-spin").addClass("fa-check"); + } + }); +} + function renewcertificate($id) { $('#'+"btnrenewicon_"+$id).removeClass("fa-check").addClass("fa-cog fa-spin"); ajaxRequest = $.ajax({ url: "", type: "post", - data: { id: $id, action: "renew"}, + data: { id: $id, action: "renewcert"}, success: function(data) { js_callbackrenew(data); $("#btnrenewicon_"+$id).removeClass("fa-cog fa-spin").addClass("fa-check"); } }); } - function togglerow($id) { ajaxRequest = $.ajax({ url: "", diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php index de5dde93505c..052ff11b736c 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php @@ -313,7 +313,7 @@ function updatevisibility() $section->addInput(new \Form_Input('desc', 'Description', 'text', $pconfig['desc'])); $activedisable = array(); $activedisable['active'] = "Active"; -$activedisable['disable'] = "Disable"; +$activedisable['disabled'] = "Disabled"; $section->addInput(new \Form_Select( 'status', 'Status', From cb886a0609e2fbca0ada3319bfc70c9a6b2bd20b Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Wed, 28 Dec 2016 02:40:00 +0100 Subject: [PATCH 10/14] acme, automatically also import the certificate chain certificates with proper refid references, validate certificatename configured does not contain spaces as acme.sh dies not handle them well --- .../files/usr/local/pkg/acme/acme.inc | 50 ++++++++++++++++--- .../files/usr/local/pkg/acme/acme_command.sh | 2 +- .../local/www/acme/acme_certificates_edit.php | 4 ++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index f261e44388cc..53b24faaccd8 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -389,23 +389,59 @@ function & get_certificate($name) { return base64_decode($cert['prv']); } - function storeCertificateCer($certificatename, $keyfile, $cerfile) { + function saveCACertificateToStore($crt) { global $config; + $crt_enc = base64_encode($crt); + if (!is_array($config['ca'])) { + $config['ca'] = array(); + } + $a_ca =& $config['ca']; + foreach($a_ca as $ca) { + if ($ca['crt'] == $crt_enc) { + return; + } + } + $subject = cert_get_subject($crt, false); + $cert = array(); + $cert['refid'] = uniqid(); + $cert['descr'] = "Acmecert: {$subject}"; + ca_import($cert, $crt); + $a_ca[] = $cert; + syslog(LOG_NOTICE, "Acme, storing new CA: {$subject}"); + } + + function storeCertificateCer($certificatename, $keyfile, $cerfile, $fullchainfile) { + global $config; + $certupdated = false; + $key = file_get_contents($keyfile); $crt = file_get_contents($cerfile); + $fullchain = ""; + if(!empty($fullchainfile)) { + $fullchain = file_get_contents($fullchainfile); + preg_match_all("/-+BEGIN CERTIFICATE(.+?)END CERTIFICATE-+/s", $fullchain, $certificatematches); + $first = true; + foreach($certificatematches[0] as $cacert) { + if ($first == true) { + $first = false; + continue; + } + saveCACertificateToStore($cacert); + } + } if (is_array($config['cert'])) { foreach ($config['cert'] as &$cert) { if ($cert['descr'] == $certificatename) { syslog(LOG_NOTICE, "Acme, storing new certificate: {$certificatename}"); - echo "update cert!"; - $cert['key'] = base64_encode($key); - $cert['crt'] = base64_encode($crt); - return true; + echo "\nupdate cert!"; + cert_import($cert, $crt, $key); + $certupdated = true; + break; } } } - //TODO: store chain to.?. - return false; + + return $certupdated; } function issue_certificate($id, $force = false, $renew = false) { diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh index 017f5ded4cc3..bd37e1fd5066 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_command.sh @@ -19,7 +19,7 @@ if ($command == "importcert") { $CA_CERT_PATH = $argv[6]; $CERT_FULLCHAIN_PATH = $argv[7]; echo "\nIMPORT CERT $certificatename, $CERT_KEY_PATH, $CERT_PATH"; - storeCertificateCer($certificatename, $CERT_KEY_PATH, $CERT_PATH); + storeCertificateCer($certificatename, $CERT_KEY_PATH, $CERT_PATH, $CERT_FULLCHAIN_PATH); $id = get_certificate_id($certificatename); $certificate = &$config['installedpackages']['acme']['certificates']['item'][$id]; diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php index 052ff11b736c..c8ed17880735 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_certificates_edit.php @@ -178,6 +178,10 @@ function customdrawcell_actions($object, $item, $itemvalue, $editable, $itemname do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + if (preg_match("/[^a-zA-Z0-9\.\-_]/", $_POST['name'])) { + $input_errors[] = "The field 'Name' contains invalid characters."; + } + if ($_POST['stats_enabled']) { $reqdfields = explode(" ", "name stats_uri"); $reqdfieldsn = explode(",", "Name,Stats Uri"); From 6c9fa133fdcb1aa7cdd6776a646e26b6c078d3e9 Mon Sep 17 00:00:00 2001 From: PiBa-NL Date: Tue, 10 Jan 2017 01:29:45 +0100 Subject: [PATCH 11/14] acme, use escapeshellarg() for shell commands, provide more sensible warnings, use fa-plus instead of fa-level-down icon --- .../files/usr/local/pkg/acme/acme.inc | 4 +- .../files/usr/local/pkg/acme/acme_gui.inc | 2 +- .../usr/local/pkg/acme/acme_htmllist.inc | 6 +- .../files/usr/local/pkg/acme/acme_sh.inc | 38 +++++++----- .../usr/local/www/acme/acme_accountkeys.php | 2 +- .../local/www/acme/acme_accountkeys_edit.php | 58 +++---------------- .../usr/local/www/acme/acme_certificates.php | 2 +- .../local/www/acme/acme_certificates_edit.php | 19 ++++-- .../local/www/acme/acme_generalsettings.php | 2 +- security/pfSense-pkg-acme/pkg-plist | 1 - 10 files changed, 53 insertions(+), 81 deletions(-) diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc index 53b24faaccd8..a36e9563f921 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc @@ -196,7 +196,7 @@ $acme_domain_validation_method['dns_me'] = array(name => "DNS-DNSMadeEasy", 'description' =>"Fill in the API Secret" ) )); -/*$acme_domain_validation_method['dns_nsupdate'] = array(name => "DNS-NSupdate", +$acme_domain_validation_method['dns_nsupdate'] = array(name => "DNS-NSupdate", 'fields' => array( 'NSUPDATE_SERVER' => array('name'=>"NSUPDATE_SERVER",'columnheader'=>"Key",'type'=>"textbox", 'description' =>"Fill in the API Key" @@ -204,7 +204,7 @@ $acme_domain_validation_method['dns_me'] = array(name => "DNS-DNSMadeEasy", 'NSUPDATE_KEY' => array('name'=>"NSUPDATE_KEY",'columnheader'=>"Id",'type'=>"textarea", 'description' =>"Fill in the API Id" ) - ));needs a file reference..*/ + )); $acme_domain_validation_method['dns_ovh'] = array(name => "DNS-ovh / kimsufi / soyoustart / runabove", 'fields' => array( 'OVH_AK' => array('name'=>"OVH_AK",'columnheader'=>"Application Key",'type'=>"textbox", diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc index 0be1b4d760bc..ac4b2854de2c 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_gui.inc @@ -32,7 +32,7 @@ $acme_icons = array( "icon" => "icon_down.gif", "iconsize" => 17), 'add' => array( - "faicon" => "fa-level-down", + "faicon" => "fa-plus", "icon" => "icon_plus.gif", "iconsize" => 17), 'delete' => array( diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc index f641207fedc4..ad5cb10b657d 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_htmllist.inc @@ -332,9 +332,9 @@ EOT } $result .= "
- - ".acmeicon('add','add another entry')." - +
"; return $result; } diff --git a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc index bfdb46af10d6..342b3c8ec296 100644 --- a/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc +++ b/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme_sh.inc @@ -80,7 +80,12 @@ class acme_sh { function generateAccountKey() { unlink_if_exists("{$this->path_account}/account.key"); $this->debug = false; - $this->execacmesh("--home {$this->acmeconf} --createAccountKey --accountkeylength 4096 --accountconf {$this->accountconfig}"); + $this->execacmesh("" + . " --home " . escapeshellarg($this->acmeconf) + . " --createAccountKey" + . " --accountkeylength 4096" + . " --accountconf " . escapeshellarg($this->accountconfig) + ); $privateKey = file_get_contents("{$this->path_account}/account.key"); return $privateKey; } @@ -88,11 +93,11 @@ class acme_sh { function registeraccount($key) { file_put_contents("{$this->path_account}/account.key", $key); $result = $this->execacmesh("" - . " --home {$this->acmeconf}" + . " --home " . escapeshellarg($this->acmeconf) . " --registeraccount" - . " --accountconf {$this->accountconfig}" + . " --accountconf " . escapeshellarg($this->accountconfig) . " --log-level 3" - . " --log {$this->acmeconf}acme_issuecert.log"); + . " --log " . escapeshellarg($this->acmeconf."acme_issuecert.log")); return $result == 0; } @@ -104,15 +109,16 @@ class acme_sh { } $certpath = "{$this->acmeconf}{$domain}{$pathadd}"; safe_mkdir($certpath); - + unlink_if_exists("{$certpath}/{$domain}.key"); $this->execacmesh("" - . " --home {$this->acmeconf}" - . " --accountconf {$this->accountconfig}" - . " --createDomainKey -d $domain" - . " --keylength $keylength" + . " --home " . escapeshellarg($this->acmeconf) + . " --accountconf " . escapeshellarg($this->accountconfig) + . " --createDomainKey -d " . escapeshellarg($domain) + . " --keylength " . escapeshellarg($keylength) . " --log-level 3" - . " --log {$this->acmeconf}acme_createdomainkey.log"); + . " --log " . escapeshellarg($this->acmeconf."acme_createdomainkey.log") + ); $privateKey = file_get_contents("{$certpath}/{$domain}.key"); return $privateKey; } @@ -125,7 +131,7 @@ class acme_sh { if ($api == "dns_manual") { $api = ""; } - $cmdparameters = " --dns {$api}"; + $cmdparameters = " --dns " . escapeshellarg($api); } else { $cmdparameters = " --webroot pfSenseacme"; } @@ -166,17 +172,17 @@ EOF; file_put_contents("{$certpath}/{$domainstosign[0]}.key", $certificatepsk); $domainstr = ""; foreach($domainstosign as $domain) { - $domainstr .= " -d {$domain}"; + $domainstr .= " -d " . escapeshellarg($domain); } $this->execacmesh("" . " --{$action} {$domainstr}" - . " --home {$this->acmeconf}" - . " --accountconf {$this->accountconfig}" + . " --home " . escapeshellarg($this->acmeconf) + . " --accountconf " . escapeshellarg($this->accountconfig) . " --force" - . " --reloadCmd {$this->acmeconf}reloadcmd.sh" + . " --reloadCmd " . escapeshellarg("{$this->acmeconf}reloadcmd.sh") . $cmdparameters . " --log-level 3" - . " --log {$this->acmeconf}acme_issuecert.log" + . " --log " . escapeshellarg("{$this->acmeconf}acme_issuecert.log") //. " > {$this->acmeconf}issue.log 2>&1" , $envvariables); $cer = "{$certpath}/{$domainstosign[0]}.cer"; diff --git a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php index c34e7a67017e..6602670e96df 100644 --- a/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php +++ b/security/pfSense-pkg-acme/files/usr/local/www/acme/acme_accountkeys.php @@ -195,7 +195,7 @@