Merge pull request #3735 from fraenki/acme_320

security/acme-client: release 3.20
This commit is contained in:
Frank Wall
2023-12-30 17:24:48 +01:00
committed by GitHub
9 changed files with 57 additions and 303 deletions
+1 -2
View File
@@ -1,6 +1,5 @@
PLUGIN_NAME= acme-client
PLUGIN_VERSION= 3.19
PLUGIN_REVISION= 1
PLUGIN_VERSION= 3.20
PLUGIN_COMMENT= ACME Client
PLUGIN_MAINTAINER= opnsense@moov.de
PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon
+10 -2
View File
@@ -9,9 +9,17 @@ Plugin Changelog
================
3.20
Added:
* add Bunny DNS API
* add support for DNSExit
* add Bunny DNS API (#3715)
* add DNSExit DNS API (#3724)
* add World4You DNS API (#3722)
Fixed:
* fix 2FA support in Synology automation (#3627)
Removed:
* remove automation: Highwinds CDN (#3626)
3.19
@@ -23,23 +23,6 @@
<type>dropdown</type>
<help>Pre-defined commands for this automation.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>
<style>method_table method_table_configd_upload_highwinds</style>
</field>
<field>
<id>action.highwinds_account_hash</id>
<label>Account Hash</label>
<type>text</type>
<help>Account hash for Highwinds API.</help>
</field>
<field>
<id>action.highwinds_access_token</id>
<label>Access Token</label>
<type>text</type>
<help>Access token for Highwinds API.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>
@@ -236,6 +219,12 @@
<type>text</type>
<help>If Synology DSM has OTP enabled, then the device ID has to be provided so that no OTP is required when running the automation.</help>
</field>
<field>
<id>action.acme_synology_dsm_devicename</id>
<label>Device Name</label>
<type>text</type>
<help>If Synology DSM has OTP enabled, then the device name has to be provided so that no OTP is required when running the automation.</help>
</field>
<field>
<id>action.acme_synology_dsm_create</id>
<label>Create certificates</label>
@@ -1656,4 +1656,19 @@
<label>Password</label>
<type>password</type>
</field>
<field>
<label>World4You</label>
<type>header</type>
<style>table_dns table_dns_world4you</style>
</field>
<field>
<id>validation.dns_world4you_username</id>
<label>User</label>
<type>text</type>
</field>
<field>
<id>validation.dns_world4you_password</id>
<label>Password</label>
<type>password</type>
</field>
</form>
@@ -48,7 +48,10 @@ class AcmeSynologyDsm extends Base implements LeAutomationInterface
$this->acme_env['SYNO_Create'] = (string)$this->config->acme_synology_dsm_create;
}
if (!empty((string)$this->config->acme_synology_dsm_deviceid)) {
$this->acme_env['SYNO_DID'] = (string)$this->config->acme_synology_dsm_deviceid;
$this->acme_env['SYNO_Device_ID'] = (string)$this->config->acme_synology_dsm_deviceid;
}
if (!empty((string)$this->config->acme_synology_dsm_devicename)) {
$this->acme_env['SYNO_Device_Name'] = (string)$this->config->acme_synology_dsm_devicename;
}
$this->acme_args[] = '--deploy-hook synology_dsm';
return true;
@@ -1,8 +1,7 @@
<?php
/*
* Copyright (C) 2020-2021 Frank Wall
* All rights reserved.
* Copyright (C) 2023 Frank Wall
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -26,20 +25,20 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\AcmeClient\LeAutomation;
namespace OPNsense\AcmeClient\LeValidation;
use OPNsense\AcmeClient\LeAutomationInterface;
use OPNsense\AcmeClient\LeValidationInterface;
use OPNsense\Core\Config;
/**
* Upload certificate to Highwinds CDN API
* World4You DNS API
* @package OPNsense\AcmeClient
*/
class ConfigdUploadHighwinds extends Base implements LeAutomationInterface
class DnsWorld4you extends Base implements LeValidationInterface
{
public function prepare()
{
$command = 'acmeclient upload_highwinds ' . $this->cert_id . ' ' . $this->config->id;
$this->command = $command;
return true;
$this->acme_env['WORLD4YOU_USERNAME'] = (string)$this->config->dns_world4you_username;
$this->acme_env['WORLD4YOU_PASSWORD'] = (string)$this->config->dns_world4you_password;
}
}
@@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/AcmeClient</mount>
<version>3.4.0</version>
<version>3.5.0</version>
<description>A secure ACME Client plugin</description>
<items>
<settings>
@@ -509,6 +509,7 @@
<dns_variomedia>Variomedia.de</dns_variomedia>
<dns_vscale>Vscale</dns_vscale>
<dns_vultr>Vultr</dns_vultr>
<dns_world4you>World4You</dns_world4you>
<dns_yandex>Yandex PDD</dns_yandex>
<dns_zilore>Zilore</dns_zilore>
<dns_zone>Zone.eu</dns_zone>
@@ -1172,6 +1173,12 @@
<dns_regru_password type="TextField">
<Required>N</Required>
</dns_regru_password>
<dns_world4you_username type="TextField">
<Required>N</Required>
</dns_world4you_username>
<dns_world4you_password type="TextField">
<Required>N</Required>
</dns_world4you_password>
</validation>
</validations>
<actions>
@@ -1199,7 +1206,6 @@
<configd_restart_gui>Restart OPNsense Web UI</configd_restart_gui>
<configd_restart_haproxy>Restart HAProxy (OPNsense plugin)</configd_restart_haproxy>
<configd_restart_nginx>Restart Nginx (OPNsense plugin)</configd_restart_nginx>
<configd_upload_highwinds>Upload certificate to Highwinds CDN</configd_upload_highwinds>
<configd_upload_sftp>Upload certificate via SFTP</configd_upload_sftp>
<configd_remote_ssh>Remote Command via SSH</configd_remote_ssh>
<acme_fritzbox>Upload certificate to FRITZ!Box router</acme_fritzbox>
@@ -1212,16 +1218,6 @@
<configd_generic>System or Plugin Command</configd_generic>
</OptionValues>
</type>
<highwinds_account_hash type="TextField">
<Required>N</Required>
<mask>/^.{1,1024}$/u</mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</highwinds_account_hash>
<highwinds_access_token type="TextField">
<Required>N</Required>
<mask>/^.{1,1024}$/u</mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</highwinds_access_token>
<sftp_host type="TextField">
<Required>N</Required>
<mask>/^.{1,255}$/u</mask>
@@ -1384,6 +1380,11 @@
<mask>/^.{1,1024}$/u</mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</acme_synology_dsm_deviceid>
<acme_synology_dsm_devicename type="TextField">
<Required>N</Required>
<mask>/^.{1,1024}$/u</mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</acme_synology_dsm_devicename>
<acme_fritzbox_url type="TextField">
<Required>N</Required>
<mask>/^.{1,1024}$/u</mask>
@@ -1,254 +0,0 @@
#!/usr/local/bin/php
<?php
/*
* Copyright (C) 2019 Frank Wall
* Copyright (C) 2015 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. 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.
*
* THIS SOFTWARE IS PROVIDED ``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 THE
* AUTHOR 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.
*/
require_once("config.inc");
require_once("certs.inc");
require_once("legacy_bindings.inc");
require_once("util.inc");
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Base;
use OPNsense\AcmeClient\AcmeClient;
$HIGHWINDS_API_URL = 'https://striketracker.highwinds.com/api/v1/accounts';
function find_certificate($acme_cert_id)
{
$modelObj = new OPNsense\AcmeClient\AcmeClient();
$configObj = Config::getInstance()->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';
$hw_method = 'POST';
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");
$hw_method = 'PUT';
// Extract certificate details
$cert = openssl_x509_parse($cert_data['cert']);
$cert_sn = (string)$cert['serialNumber'];
$hw_cert_sn = (string)$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';
}
log_error("AcmeClient: cert serial is different in Highwinds API, updating...");
$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 => $hw_method,
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);
@@ -91,12 +91,6 @@ parameters:%s
type:script
message:registering an account
[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
[upload-sftp]
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php
parameters:--certificates=%s --automation-id=%s