@@ -702,9 +702,20 @@
Restart OPNsense Web UI
Restart HAProxy (OPNsense plugin)
Restart Nginx (OPNsense plugin)
+ Upload certificate to Highwinds CDN
System or Plugin Command (select below)
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
/^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/
diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml
index 6ed9f6393..472b15b89 100644
--- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml
+++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt
index d46c22306..54fd0c35b 100644
--- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt
+++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt
@@ -1,6 +1,6 @@
{#
-Copyright (C) 2017 Frank Wall
+Copyright (C) 2017-2019 Frank Wall
OPNsense® is Copyright © 2014-2015 by Deciso B.V.
All rights reserved.
@@ -66,7 +66,7 @@ POSSIBILITY OF SUCH DAMAGE.
@@ -97,4 +97,4 @@ POSSIBILITY OF SUCH DAMAGE.
{# include dialogs #}
-{{ partial("layout_partials/base_dialog",['fields':formDialogAction,'id':'DialogAction','label':lang._('Edit Restart Action')])}}
+{{ partial("layout_partials/base_dialog",['fields':formDialogAction,'id':'DialogAction','label':lang._('Edit Automation')])}}
diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php
index 77f0a39e7..937b04b90 100755
--- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php
+++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php
@@ -1138,7 +1138,10 @@ function run_restart_actions($certlist, $modelObj)
continue;
}
// Store by UUID, automatically eliminates duplicates.
- $restart_actions[$_action] = $action;
+ $_data = array();
+ $_data['obj'] = $action;
+ $_data['cert_id'] = $certObj->id;
+ $restart_actions[$_action] = $_data;
}
}
}
@@ -1147,7 +1150,10 @@ function run_restart_actions($certlist, $modelObj)
// Run the collected restart actions.
if (!empty($restart_actions) and is_array($restart_actions)) {
// Extract cert object
- foreach ($restart_actions as $action) {
+ foreach ($restart_actions as $_action) {
+ $action = $_action['obj'];
+ $cert_id = $_action['cert_id'];
+ $action_id = $action->id;
// Run pre-defined or custom command?
log_error("AcmeClient: running restart action: " . $action->name);
switch ((string)$action->type) {
@@ -1160,6 +1166,9 @@ function run_restart_actions($certlist, $modelObj)
case 'restart_nginx':
$response = $backend->configdRun("nginx restart");
break;
+ case 'upload_highwinds':
+ $response = $backend->configdRun("acmeclient upload_highwinds ${cert_id} ${action_id}");
+ break;
case 'configd':
// Make sure a configd command was specified.
if (empty((string)$action->configd)) {
diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php
new file mode 100755
index 000000000..03d6e6236
--- /dev/null
+++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php
@@ -0,0 +1,247 @@
+#!/usr/local/bin/php
+object();
+ if (isset($configObj->OPNsense->AcmeClient->certificates) && $configObj->OPNsense->AcmeClient->certificates->count() > 0) {
+ foreach ($configObj->OPNsense->AcmeClient->certificates->children() as $certObj) {
+ $cert_id = (string)$certObj->id;
+ $cert_name = (string)$certObj->name;
+ if ($cert_id == $acme_cert_id) {
+ if ($certObj->enabled == 0) {
+ log_error("AcmeClient: certificate ${cert_name} is disabled, ignoring upload request");
+ return None;
+ }
+ if (isset($certObj->certRefId)) {
+ $data = array();
+ $data['name'] = $cert_name;
+ $data['refid'] = (string)$certObj->certRefId;
+ return $data;
+ } else {
+ log_error("AcmeClient: certificate ${cert_name} could not be found in trust storage, ignoring upload request");
+ break;
+ }
+ }
+ }
+ return None;
+ }
+}
+
+function export_certificate($cert_refid) {
+ $configObj = Config::getInstance()->object();
+ foreach ($configObj->cert as $cert) {
+ if ($cert_refid == (string)$cert->refid) {
+ $cert_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt)));
+ $key_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->prv)));
+ // check if a CA is linked
+ if (!empty((string)$cert->caref)) {
+ $cert = (array)$cert;
+ $ca = ca_chain($cert);
+ $ca_content = $ca;
+ }
+ $result = array();
+ $result['cert'] = $cert_content;
+ $result['key'] = $key_content;
+ $result['ca'] = $ca_content;
+ return $result;
+ }
+ }
+ log_error("AcmeClient: cert with refid ${cert_refid} not found in trust storage");
+ return None;
+}
+
+function upload_certificate($cert_name,$cert_refid,$acme_cert_id,$acme_automation_id) {
+ $modelObj = new OPNsense\AcmeClient\AcmeClient;
+ $configObj = Config::getInstance()->object();
+ if (isset($configObj->OPNsense->AcmeClient->actions) && $configObj->OPNsense->AcmeClient->actions->count() > 0) {
+ foreach ($configObj->OPNsense->AcmeClient->actions->children() as $automObj) {
+ $autom_id = (string)$automObj->id;
+ if ($autom_id == $acme_automation_id) {
+ if ($automObj->enabled == 0) {
+ log_error("AcmeClient: ignoring disabled upload job for cert ${cert_name}");
+ return None;
+ }
+ if (isset($automObj->highwinds_account_hash) && isset($automObj->highwinds_access_token)) {
+ $hw_account_hash = (string)$automObj->highwinds_account_hash;
+ $hw_access_token = (string)$automObj->highwinds_access_token;
+ $cert_data = export_certificate($cert_refid);
+ if ($cert_data !== None) {
+ $hw_result = hw_upload_certificate($hw_account_hash,$hw_access_token,$cert_name,$cert_data);
+ if ($hw_result !== None) {
+ return true;
+ }
+ }
+ } else {
+ log_error("AcmeClient: upload job for cert ${cert_name} is incomplete, missing Highwinds configuration");
+ return None;
+ }
+ }
+ }
+ return None;
+ }
+}
+
+function hw_list_certificates($account_hash,$access_token)
+{
+ global $HIGHWINDS_API_URL;
+ $curl = curl_init();
+ curl_setopt_array($curl, array(
+ CURLOPT_URL => "${HIGHWINDS_API_URL}/${account_hash}/certificates",
+ CURLOPT_CUSTOMREQUEST => 'GET',
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_MAXREDIRS => 1,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+ CURLOPT_HTTPHEADER => array(
+ "Authorization: Bearer ${access_token}",
+ "Content-Type: application/json",
+ "User-Agent: OPNsense Firewall",
+ "X-Application-Id: OPNsense Firewall"
+ )
+ ));
+ $response = curl_exec($curl);
+ $err = curl_error($curl);
+ $info = curl_getinfo($curl);
+ curl_close($curl);
+ $http_code = $info['http_code'];
+ if ($http_code != 200 || $err) {
+ log_error("AcmeClient: failed to access Highwinds API, HTTP Code: ${http_code}, error ${err}");
+ return None;
+ }
+ return json_decode($response);
+}
+
+function hw_get_certificate($account_hash,$access_token,$cert_name)
+{
+ $certificates = hw_list_certificates($account_hash,$access_token);
+ if ($certificates !== None) {
+ foreach ($certificates->list as $cert) {
+ if ($cert->commonName == $cert_name) {
+ return $cert;
+ }
+ }
+ }
+ return None;
+}
+
+function hw_upload_certificate($account_hash,$access_token,$cert_name,$cert_data)
+{
+ global $HIGHWINDS_API_URL;
+ // Check current status of certificate at Highwinds
+ $hw_cert = hw_get_certificate($account_hash,$access_token,$cert_name);
+ $hw_url = 'certificates';
+ if ($hw_cert == None) {
+ log_error("AcmeClient: cert for ${cert_name} not found in Highwinds API, starting upload...");
+ } else {
+ log_error("AcmeClient: cert for ${cert_name} found in Highwinds API");
+
+ // Extract certificate details
+ $cert = openssl_x509_parse($cert_data['cert']);
+ $cert_sn = (int)$cert['serialNumber'];
+ $hw_cert_sn = (int)$hw_cert->certificateInformation->serialNumber;
+ $hw_cert_id = $hw_cert->id;
+
+ // Compare local and remote certificates
+ if ($cert_sn == $hw_cert_sn) {
+ log_error("AcmeClient: cert ${cert_name} has same serial in Highwinds API, not updating (${cert_sn})");
+ return None;
+ }
+ $hw_url = "${hw_url}/${hw_cert_id}";
+ }
+
+ // adjust data format for Highwinds API
+ $cert_post = json_encode(array('certificate' => $cert_data['cert'], 'key' => $cert_data['key'], 'caBundle' => $cert_data['ca']));
+
+ $curl = curl_init();
+ curl_setopt_array($curl, array(
+ CURLOPT_URL => "${HIGHWINDS_API_URL}/${account_hash}/${hw_url}",
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_POSTFIELDS => (string)$cert_post,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_MAXREDIRS => 1,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_SAFE_UPLOAD => true,
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+ CURLOPT_HTTPHEADER => array(
+ "Authorization: Bearer ${access_token}",
+ "Content-Type: application/json",
+ "User-Agent: OPNsense Firewall",
+ "X-Application-Id: OPNsense Firewall",
+ "Expect:"
+ )
+ ));
+ $response = curl_exec($curl);
+ $err = curl_error($curl);
+ $info = curl_getinfo($curl);
+ curl_close($curl);
+ $http_code = $info['http_code'];
+ if ($http_code != 200 || $err) {
+ log_error("AcmeClient: Failed to upload cert ${cert_name} to Highwinds API, HTTP Code: ${http_code}, error ${err}");
+ return None;
+ }
+ return json_decode($response);
+}
+
+// Evaluate CLI arguments
+$options = getopt("a:c:");
+if (!isset($options["a"]) or !isset($options["c"])) {
+ print "ERROR: not enough arguments\n";
+ exit(1);
+}
+$acme_cert_id = $options["c"];
+$acme_automation_id = $options["a"];
+
+// Search certificate in configuration
+$cert_data = find_certificate($acme_cert_id);
+if ($cert_data == None) {
+ log_error("AcmeClient: ignoring cert ID ${acme_cert_id}");
+ exit(1);
+} else {
+ // Upload certificate (if required)
+ $upload_result = upload_certificate($cert_data['name'], $cert_data['refid'], $acme_cert_id, $acme_automation_id);
+ if ($upload_result === None) {
+ log_error("AcmeClient: cert ID ${acme_cert_id} was neither uploaded nor updated");
+ } else {
+ log_error("AcmeClient: cert ID ${acme_cert_id} was uploaded or updated");
+ }
+}
+exit(0);
diff --git a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf
index 3bf17b4b0..b44a19be7 100644
--- a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf
+++ b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf
@@ -65,3 +65,9 @@ parameters:
type:script
message:cronjob running to sign or renew certificates
description:Renew Let's Encrypt certificates
+
+[upload_highwinds]
+command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php
+parameters:-c %s -a %s
+type:script
+message:uploading a certificate to highwinds