diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index 0a6e2f62f..9f906ec38 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.23 +PLUGIN_VERSION= 1.24 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de PLUGIN_DEPENDS= acme.sh diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php index 88df62cdc..99cad52cb 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php @@ -62,6 +62,16 @@ class CertificatesController extends ApiMutableModelControllerBase public function delAction($uuid) { + # Remove the cert from list of certs known to acme.sh. + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); + if ($node != null) { + $cert_id = $node->id; + $backend = new Backend(); + $response = $backend->configdRun("acmeclient remove-cert {$cert_id}"); + } + } return $this->delBase('certificates.certificate', $uuid); } @@ -99,6 +109,26 @@ class CertificatesController extends ApiMutableModelControllerBase return $result; } + /** + * remove private key from certificate by uuid + * @param $uuid item unique id + * @return array status + */ + public function removekeyAction($uuid) + { + $result = array("result"=>"failed"); + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); + if ($node != null) { + $cert_id = $node->id; + $backend = new Backend(); + $response = $backend->configdRun("acmeclient remove-key {$cert_id}"); + } + } + return $result; + } + /** * revoke certificate by uuid * @param $uuid item unique id diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt index 35d2bae83..fc47d510f 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt @@ -47,6 +47,7 @@ POSSIBILITY OF SUCH DAMAGE. toggle:'/api/acmeclient/certificates/toggle/', sign:'/api/acmeclient/certificates/sign/', revoke:'/api/acmeclient/certificates/revoke/', + removekey:'/api/acmeclient/certificates/removekey/', }; var gridopt = { @@ -61,7 +62,8 @@ POSSIBILITY OF SUCH DAMAGE. "" + "" + "" + - ""; + "" + + ""; }, "rowtoggle": function (column, row) { if (parseInt(row[column.id], 2) == 1) { @@ -314,7 +316,7 @@ POSSIBILITY OF SUCH DAMAGE. }); // sign cert - // TODO: this should block other sign/revoke actions + // TODO: this should block other acme.sh actions grid_certificates.find(".command-sign").on("click", function(e) { if (gridParams['sign'] != undefined) { @@ -336,7 +338,7 @@ POSSIBILITY OF SUCH DAMAGE. }); // revoke cert - // TODO: this should block other sign/revoke actions + // TODO: this should block other acme.sh actions grid_certificates.find(".command-revoke").on("click", function(e) { if (gridParams['revoke'] != undefined) { @@ -355,6 +357,26 @@ POSSIBILITY OF SUCH DAMAGE. } }); + // remove private key + // TODO: this should block other acme.sh actions + grid_certificates.find(".command-removekey").on("click", function(e) + { + if (gridParams['removekey'] != undefined) { + var uuid=$(this).data("row-id"); + stdDialogConfirm('{{ lang._('Confirmation Required') }}', + '{{ lang._('Really remove the private key?%s%sThe certificate will be completely reset. This is useful when the private key has been compromised or when you have changed the key options and want to regenerate the private key.%sNote that you have to revalidate the certificate afterwards in order to create a new private key and a matching certificate.') | format('
', '
', '
') }}', + '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { + ajaxCall(url=gridParams['removekey'] + uuid, + sendData={},callback=function(data,status){ + // reload grid after sign + $("#"+gridId).bootgrid("reload"); + }); + }, 'danger'); + } else { + console.log("[grid] action removekey missing") + } + }); + }); /*********************************************************************** @@ -363,7 +385,7 @@ POSSIBILITY OF SUCH DAMAGE. /** * Sign or renew ALL certificates - * TODO: this should block other sign/revoke actions + * TODO: this should block other acme.sh actions */ $("#signallcertsAct").click(function(){ //$("#signallcertsAct_progress").addClass("fa fa-spinner fa-pulse"); 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 78f63dbad..ae5ac9d26 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php @@ -77,20 +77,25 @@ if (isset($options["C"])) { // Run the specified action switch ($options["a"]) { case 'sign': - //$result = sign_or_renew_cert($options["c"]); $result = cert_action_validator($options["c"]); echo json_encode(array('status'=>$result)); break; case 'renew': - //$result = sign_or_renew_cert($options["c"]); + $result = cert_action_validator($options["c"]); + echo json_encode(array('status'=>$result)); + break; + case 'remove': + $result = cert_action_validator($options["c"]); + echo json_encode(array('status'=>$result)); + break; + case 'removekey': $result = cert_action_validator($options["c"]); echo json_encode(array('status'=>$result)); break; case 'revoke': - //$result = revoke_cert($options["c"]); $result = cert_action_validator($options["c"]); echo json_encode(array('status'=>$result)); - exit(1); + break; default: echo "ERROR: invalid argument specified\n"; log_error("invalid argument specified"); @@ -126,10 +131,14 @@ function cert_action_validator($opt_cert_id) if (isset($options["A"]) or ((string)$cert_id == (string)$opt_cert_id)) { // Ignore disabled certificates if ($certObj->enabled == 0) { + // Always ignore disabled certs when working on ALL certs. if (isset($options["A"])) { continue; // skip to next item } - return(1); // Cert is disabled, skip it. + // Allow only "revoke", "remove" and "removekey" for disabled certs. + if (!in_array($options["a"], ['remove','removekey','revoke'])) { + return(1); // Cert is disabled, skip it. + } } // Extract Account from referenced obj @@ -179,6 +188,37 @@ function cert_action_validator($opt_cert_id) } } + // Cert is being removed from the GUI, delete all traces. + if ($options["a"] == "remove") { + // Start acme client to remove the certificate + $rev_result = remove_cert($certObj); + if (!$rev_result) { + log_error("AcmeClient: successfully removed acme.sh certificate configuration for " . (string)$certObj->name); + return(0); // Success! + } else { + log_error("AcmeClient: failed to remove acme.sh certificate configuration for " . (string)$certObj->name); + return(1); + } + } + + // Remove private key + // NOTE: Although the user requested to remove the private key, + // we simply perform a full cert removal because without the + // matching private key the cert is useless. + if ($options["a"] == "removekey") { + // Start acme client to remove the certificate + $rev_result = remove_cert($certObj); + if (!$rev_result) { + log_error("AcmeClient: successfully removed the private key and reset certificate " . (string)$certObj->name); + // Reset certificate state, treat it like a new certificate. + log_cert_acme_status($certObj, $modelObj, '100'); + return(0); // Success! + } else { + log_error("AcmeClient: failed to remove the private key and reset certificate " . (string)$certObj->name); + return(1); + } + } + // Make sure we found the configured validation method if ($ref_found == true) { // Was a revocation requested? @@ -945,7 +985,47 @@ function revoke_cert($certObj, $valObj, $acctObj) . $ecc_param; $result = mwexec($acmecmd); - // TODO: maybe clear lastUpdate value? + // Simply return acme clients exit code + return($result); +} + +// Remove a cert from list of certs known to acme.sh. +function remove_cert($certObj) +{ + // Prepare optional parameters for acme-client + $acme_args = eval_optional_acme_args(); + + // Generate certificate filenames + $cert_id = (string)$certObj->id; + + // Check if EC certificate is used, if yes add the --ecc parameter to acme client + $key_length = (string) $certObj->keyLength; + $ecc_param = " "; + if ($key_length == 'key_ec256' || $key_length == 'key_ec384') { + $ecc_param = "--ecc"; + } + + // Run acme client + $acmecmd = "/usr/local/sbin/acme.sh " + . implode(" ", $acme_args) . " " + . "--remove " + . "--domain " . (string)$certObj->name . " " + . "--home /var/etc/acme-client/home " + . $ecc_param; + $result = mwexec($acmecmd); + + $cert_files = [ + "/var/etc/acme-client/keys/${cert_id}/private.key", + "/var/etc/acme-client/certs/${cert_id}/cert.pem", + "/var/etc/acme-client/certs/${cert_id}/chain.pem", + "/var/etc/acme-client/certs/${cert_id}/fullchain.pem", + ]; + + foreach ($cert_files as $_file) { + if (file_exists($_file)) { + unlink($_file); + } + } // Simply return acme clients exit code return($result); 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 eddddd48a..99858b1aa 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 @@ -53,6 +53,18 @@ parameters:%s type:script message:revoking a certificate +[remove-cert] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a remove -c +parameters:%s +type:script +message:removing a certificate + +[remove-key] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a removekey -c +parameters:%s +type:script +message:removing a certificate private key + [sign-all-certs] command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a sign -A parameters: