security/acme-client: merge version 1.2 from master

This commit is contained in:
Franco Fichtner 2017-03-14 06:48:52 +01:00
parent 071ba27f3c
commit 2df92251f0
11 changed files with 146 additions and 46 deletions

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= acme-client
PLUGIN_VERSION= 1.1
PLUGIN_VERSION= 1.2
PLUGIN_COMMENT= Let's Encrypt client
PLUGIN_MAINTAINER= opnsense@moov.de

View file

@ -203,7 +203,7 @@ class CertificatesController extends ApiControllerBase
$grid = new UIModelGrid($mdlAcme->certificates->certificate);
return $grid->fetchBindRequest(
$this->request,
array("enabled", "name", "altNames", "description"),
array("enabled", "name", "altNames", "description", "lastUpdate", "statusCode", "statusLastUpdate"),
"name"
);
}

View file

@ -125,7 +125,7 @@ class SettingsController extends ApiMutableModelControllerBase
$mdlAcme = $this->getModel();
// Check if the required plugin is installed
if ((string)$mdlAcme->isPluginInstalled('os-haproxy') != "1") {
if ((string)$mdlAcme->isPluginInstalled('haproxy') != "1") {
$this->getLogger()->error("LE check: HAProxy plugin is NOT installed, skipping integration");
return($result);
}
@ -322,6 +322,11 @@ class SettingsController extends ApiMutableModelControllerBase
(string)$validation->method == "http01" and
(string)$validation->http_service == "haproxy") {
//$this->getLogger()->error("LE HAProxy DEBUG: checking validation method: " . (string)$validation->name);
// Check if HAProxy frontends were specified.
if (empty((string)$validation->http_haproxyFrontends)) {
// Skip item, no HAProxy frontends were specified.
continue;
}
$_frontends = explode(',', $validation->http_haproxyFrontends);
// Walk through all linked frontends.
foreach ($_frontends as $_frontend) {

View file

@ -28,9 +28,9 @@
</field>
<field>
<id>certificate.account</id>
<label>CA Account</label>
<label>LE Account</label>
<type>dropdown</type>
<help><![CDATA[Set the CA account to use for this certificate.]]></help>
<help><![CDATA[Set the Let's Encrypt account to use for this certificate.]]></help>
</field>
<field>
<id>certificate.validationMethod</id>

View file

@ -26,6 +26,7 @@
<field>
<label>HTTP-01</label>
<type>header</type>
<style>method_table method_table_http01</style>
</field>
<field>
<id>validation.http_service</id>
@ -36,6 +37,7 @@
<field>
<label>HTTP-01/OPNsense</label>
<type>header</type>
<style>method_table method_table_http01</style>
</field>
<field>
<id>validation.http_opn_autodiscovery</id>
@ -61,6 +63,7 @@
<field>
<label>HTTP-01/HAProxy</label>
<type>header</type>
<style>method_table method_table_http01</style>
</field>
<field>
<id>validation.http_haproxyInject</id>
@ -93,6 +96,7 @@
<field>
<label>DNS-01</label>
<type>header</type>
<style>method_table method_table_dns01</style>
</field>
<field>
<id>validation.dns_service</id>
@ -109,6 +113,7 @@
<field>
<label>DNS-01/Alwaysdata</label>
<type>header</type>
<style>table_dns table_dns_ad</style>
</field>
<field>
<id>validation.dns_ad_key</id>
@ -119,6 +124,7 @@
<field>
<label>DNS-01/aliyun</label>
<type>header</type>
<style>table_dns table_dns_ali</style>
</field>
<field>
<id>validation.dns_ali_key</id>
@ -135,6 +141,7 @@
<field>
<label>DNS-01/AWS Route53</label>
<type>header</type>
<style>table_dns table_dns_aws</style>
</field>
<field>
<id>validation.dns_aws_id</id>
@ -151,6 +158,7 @@
<field>
<label>DNS-01/Cloudflare</label>
<type>header</type>
<style>table_dns table_dns_cf</style>
</field>
<field>
<id>validation.dns_cf_email</id>
@ -167,6 +175,7 @@
<field>
<label>DNS-01/CloudXNS</label>
<type>header</type>
<style>table_dns table_dns_cx</style>
</field>
<field>
<id>validation.dns_cx_key</id>
@ -183,6 +192,7 @@
<field>
<label>DNS-01/DNSPod</label>
<type>header</type>
<style>table_dns table_dns_dp</style>
</field>
<field>
<id>validation.dns_dp_id</id>
@ -199,6 +209,7 @@
<field>
<label>DNS-01/GoDaddy</label>
<type>header</type>
<style>table_dns table_dns_gd</style>
</field>
<field>
<id>validation.dns_gd_key</id>
@ -215,6 +226,7 @@
<field>
<label>DNS-01/IPSConfig</label>
<type>header</type>
<style>table_dns table_dns_ispconfig</style>
</field>
<field>
<id>validation.dns_ispconfig_user</id>
@ -243,6 +255,7 @@
<field>
<label>DNS-01/lexicon</label>
<type>header</type>
<style>table_dns table_dns_lexicon</style>
</field>
<field>
<id>validation.dns_lexicon_provider</id>
@ -265,6 +278,7 @@
<field>
<label>DNS-01/LuaDNS</label>
<type>header</type>
<style>table_dns table_dns_lua</style>
</field>
<field>
<id>validation.dns_lua_email</id>
@ -281,6 +295,7 @@
<field>
<label>DNS-01/DNSMadeEasy</label>
<type>header</type>
<style>table_dns table_dns_me</style>
</field>
<field>
<id>validation.dns_me_key</id>
@ -297,6 +312,7 @@
<field>
<label>DNS-01/nsupdate</label>
<type>header</type>
<style>table_dns table_dns_nsupdate</style>
</field>
<field>
<id>validation.dns_nsupdate_server</id>
@ -313,6 +329,7 @@
<field>
<label>DNS-01/OVH</label>
<type>header</type>
<style>table_dns table_dns_ovh</style>
</field>
<field>
<id>validation.dns_ovh_app_key</id>
@ -341,6 +358,7 @@
<field>
<label>DNS-01/PowerDNS</label>
<type>header</type>
<style>table_dns table_dns_pdns</style>
</field>
<field>
<id>validation.dns_pdns_url</id>

View file

@ -96,45 +96,7 @@ class AcmeClient extends BaseModel
*/
public function isPluginInstalled($name)
{
// NOTE: Based on infoAction() from Core/Api/FirmwareController.php
// FIXME: Should be replaced by a Core function sooner or later.
$backend = new Backend();
$keys = array('name', 'version', 'comment', 'flatsize', 'locked', 'license');
$plugins = array();
// Only check local package data for performance reasons
$current = $backend->configdRun("firmware local");
$current = explode("\n", trim($current));
foreach ($current as $line) {
/* package infos are flat lists with 3 pipes as delimiter */
$expanded = explode('|||', $line);
$translated = array();
$index = 0;
if (count($expanded) != count($keys)) {
continue;
}
foreach ($keys as $key) {
$translated[$key] = $expanded[$index++];
}
/* mark local packages as "installed" */
$translated['installed'] = "1";
/* figure out local and remote plugins */
$plugin = explode('-', $translated['name']);
if (count($plugin)) {
if ($plugin[0] == 'os' || $plugin[0] == 'ospriv') {
$plugins[$translated['name']] = $translated;
}
}
}
if (isset($plugins[$name]) and $plugins[$name]['installed'] == "1") {
return 1; // TRUE, is installed
} else {
return 0; // FALSE, is not installed
}
return trim($backend->configdRun('firmware plugin ' . escapeshellarg($name)));
}
}

View file

@ -224,6 +224,18 @@
<lastUpdate type="IntegerField">
<Required>N</Required>
</lastUpdate>
<!-- hidden field; status of last operation -->
<statusCode type="IntegerField">
<Required>N</Required>
<!-- XXX: enable once data migration is working -->
<!-- <default>100</default> -->
<MinimumValue>100</MinimumValue>
<MaximumValue>1000</MaximumValue>
</statusCode>
<!-- hidden field; timestamp for statusCode -->
<statusLastUpdate type="IntegerField">
<Required>N</Required>
</statusLastUpdate>
</certificate>
</certificates>
<validations>

View file

@ -69,6 +69,52 @@ POSSIBILITY OF SUCH DAMAGE.
} else {
return "<span style=\"cursor: pointer;\" class=\"fa fa-square-o command-toggle\" data-value=\"0\" data-row-id=\"" + row.uuid + "\"></span>";
}
},
"certdate": function (column, row) {
if (row.lastUpdate == "" || row.lastUpdate == undefined) {
return "{{ lang._('pending') }}";
} else {
var certdate = new Date(row.lastUpdate*1000);
return certdate.toLocaleString();
}
},
"acmestatus": function (column, row) {
if (row.statusCode == "" || row.statusCode == undefined) {
// fallback to lastUpdate value (unset if cert was never issued/imported)
if (row.lastUpdate == "" || row.lastUpdate == undefined) {
return "{{ lang._('unknown') }}";
} else {
return "{{ lang._('OK') }}";
}
} else if (row.statusCode == "100") {
return "{{ lang._('unknown') }}";
} else if (row.statusCode == "200") {
return "{{ lang._('OK') }}";
} else if (row.statusCode == "250") {
return "{{ lang._('cert revoked') }}";
} else if (row.statusCode == "300") {
return "{{ lang._('configuration error') }}";
} else if (row.statusCode == "400") {
return "{{ lang._('validation failed') }}";
} else if (row.statusCode == "500") {
return "{{ lang._('internal error') }}";
} else {
return "{{ lang._('unknown') }}";
}
},
"acmestatusdate": function (column, row) {
if (row.statusLastUpdate == "" || row.statusCode == undefined) {
// fallback to lastUpdate value
if (row.lastUpdate == "" || row.lastUpdate == undefined) {
return "{{ lang._('unknown') }}";
} else {
var legacydate = new Date(row.lastUpdate*1000);
return legacydate.toLocaleString();
}
} else {
var statusdate = new Date(row.statusLastUpdate*1000);
return statusdate.toLocaleString();
}
}
},
};
@ -339,6 +385,9 @@ POSSIBILITY OF SUCH DAMAGE.
<th data-column-id="name" data-type="string">{{ lang._('Certificate Name') }}</th>
<th data-column-id="altNames" data-type="string">{{ lang._('Multi-Domain (SAN)') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="lastUpdate" data-type="string" data-formatter="certdate">{{ lang._('Issue/Renewal Date') }}</th>
<th data-column-id="statusCode" data-type="string" data-formatter="acmestatus">{{ lang._('Last Acme Status') }}</th>
<th data-column-id="statusLastUpdate" data-type="string" data-formatter="acmestatusdate">{{ lang._('Last Acme Run') }}</th>
<th data-column-id="commands" data-width="11em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
</tr>

View file

@ -175,7 +175,7 @@ POSSIBILITY OF SUCH DAMAGE.
});
} else {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
type: BootstrapDialog.TYPE_INFO,
title: "{{ lang._('acme-client config test result') }}",
message: "{{ lang._('Your acme-client config contains no errors.') }}",
draggable: true

View file

@ -48,6 +48,22 @@ POSSIBILITY OF SUCH DAMAGE.
}
);
// hook into on-show event for dialog to extend layout.
$('#DialogValidation').on('shown.bs.modal', function (e) {
$("#validation\\.dns_service").change(function(){
var service_id = 'table_' + $(this).val() ;
$(".table_dns").hide();
if ($("#validation\\.method").val() == 'dns01') {
$("."+service_id).show();
}
});
$("#validation\\.method").change(function(){
$(".method_table").hide();
$(".method_table_"+$(this).val()).show();
$("#validation\\.dns_service").change();
});
$("#validation\\.method").change();
})
});
</script>

View file

@ -158,6 +158,7 @@ function cert_action_validator($opt_cert_id)
} else {
//echo "DEBUG: account registration failed\n";
log_error("AcmeClient: account registration failed");
log_cert_acme_status($certObj, $modelObj, '400');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -166,6 +167,7 @@ function cert_action_validator($opt_cert_id)
} else {
//echo "DEBUG: account not found\n";
log_error("AcmeClient: account not found");
log_cert_acme_status($certObj, $modelObj, '300');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -193,10 +195,12 @@ function cert_action_validator($opt_cert_id)
// Start acme client to revoke the certificate
$rev_result = revoke_cert($certObj, $valObj, $acctObj);
if (!$rev_result) {
log_cert_acme_status($certObj, $modelObj, '250');
return(0); // Success!
} else {
// Revocation failure
log_error("AcmeClient: revocation for certificate failed");
log_cert_acme_status($certObj, $modelObj, '400');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -215,8 +219,10 @@ function cert_action_validator($opt_cert_id)
//echo "DEBUG: cert import done\n";
// Prepare certificate for restart action
$restart_certs[] = $certObj;
log_cert_acme_status($certObj, $modelObj, '200');
} else {
log_error("AcmeClient: unable to import certificate: " . (string)$certObj->name);
log_cert_acme_status($certObj, $modelObj, '500');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -227,6 +233,7 @@ function cert_action_validator($opt_cert_id)
} else {
// validation failure
log_error("AcmeClient: validation for certificate failed: " . (string)$certObj->name);
log_cert_acme_status($certObj, $modelObj, '400');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -234,6 +241,7 @@ function cert_action_validator($opt_cert_id)
}
} else {
log_error("AcmeClient: invalid validation method specified: " . (string)$valObj->method);
log_cert_acme_status($certObj, $modelObj, '300');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -241,6 +249,7 @@ function cert_action_validator($opt_cert_id)
}
} else {
log_error("AcmeClient: validation method not found for cert " . $certObj->name);
log_cert_acme_status($certObj, $modelObj, '300');
if (isset($options["A"])) {
continue; // skip to next item
}
@ -887,8 +896,11 @@ function run_restart_actions($certlist, $modelObj)
}
// Extract restart actions
$_actions = explode(',', $certObj->restartActions);
if (empty($_actions)) {
// No restart actions configured.
continue;
}
// Walk through all linked restart actions.
$_actions = explode(',', $certObj->restartActions);
foreach ($_actions as $_action) {
// Extract restart action
$action = $modelObj->getByActionID($_action);
@ -1007,6 +1019,32 @@ function run_restart_actions($certlist, $modelObj)
return($return);
}
/* Update certificate object to log the status of the current acme run.
* Supported status codes are:
* 100 pending
* 200 issue/renew OK
* 250 certificate revoked
* 300 configuration error (validation method, account, ...)
* 400 issue/renew failed
* 500 internal error (code issues, bad luck, unexpected errors, ...)
* Feel free to add more status codes to make it more useful.
*/
function log_cert_acme_status($certObj, $modelObj, $statusCode)
{
$uuid = $certObj->attributes()->uuid;
$node = $modelObj->getNodeByReference('certificates.certificate.' . $uuid);
if ($node != null) {
$node->statusCode = $statusCode;
$node->statusLastUpdate = time();
// serialize to config and save
$modelObj->serializeToConfig();
Config::getInstance()->save();
} else {
log_error("AcmeClient: unable to update acme status for certificate " . (string)$certObj->name);
return(1);
}
}
// taken from certs.inc
function local_cert_get_subject_array($str_crt, $decode = true)
{