From 8fe0f3a899e29c2ff684710c683450dbb6075add Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Thu, 27 Aug 2020 21:57:42 +0200 Subject: [PATCH] security/acme-client: release 2.0 --- security/acme-client/Makefile | 2 +- security/acme-client/pkg-descr | 318 ++++ .../AcmeClient/Api/AccountsController.php | 26 +- .../AcmeClient/Api/CertificatesController.php | 18 +- .../AcmeClient/Api/ServiceController.php | 1 - .../AcmeClient/Api/SettingsController.php | 4 +- .../AcmeClient/forms/dialogCertificate.xml | 4 +- .../OPNsense/AcmeClient/forms/settings.xml | 8 +- .../library/OPNsense/AcmeClient/LeAccount.php | 263 +++ .../OPNsense/AcmeClient/LeAutomation/Base.php | 97 + .../AcmeClient/LeAutomation/Configd.php | 51 + .../AcmeClient/LeAutomation/RestartGui.php | 45 + .../LeAutomation/RestartHaproxy.php | 44 + .../AcmeClient/LeAutomation/RestartNginx.php | 44 + .../LeAutomation/UploadHighwinds.php | 45 + .../AcmeClient/LeAutomation/UploadSftp.php | 45 + .../AcmeClient/LeAutomationFactory.php | 81 + .../AcmeClient/LeAutomationInterface.php | 43 + .../OPNsense/AcmeClient/LeCertificate.php | 650 +++++++ .../library/OPNsense/AcmeClient/LeCommon.php | 214 +++ .../library/OPNsense/AcmeClient/LeUtils.php | 189 ++ .../OPNsense/AcmeClient/LeValidation/Base.php | 298 +++ .../LeValidation/Dns1984hosting.php | 45 + .../AcmeClient/LeValidation/DnsAcmedns.php | 47 + .../AcmeClient/LeValidation/DnsAcmeproxy.php | 46 + .../AcmeClient/LeValidation/DnsAd.php | 44 + .../AcmeClient/LeValidation/DnsAli.php | 45 + .../AcmeClient/LeValidation/DnsArvan.php | 44 + .../AcmeClient/LeValidation/DnsAutodns.php | 46 + .../AcmeClient/LeValidation/DnsAws.php | 45 + .../AcmeClient/LeValidation/DnsAzure.php | 47 + .../AcmeClient/LeValidation/DnsCf.php | 49 + .../AcmeClient/LeValidation/DnsCloudns.php | 46 + .../AcmeClient/LeValidation/DnsCn.php | 45 + .../AcmeClient/LeValidation/DnsCx.php | 45 + .../AcmeClient/LeValidation/DnsCyon.php | 45 + .../AcmeClient/LeValidation/DnsDa.php | 45 + .../AcmeClient/LeValidation/DnsDgon.php | 44 + .../AcmeClient/LeValidation/DnsDnsimple.php | 44 + .../AcmeClient/LeValidation/DnsDo.php | 45 + .../AcmeClient/LeValidation/DnsDoapi.php | 44 + .../AcmeClient/LeValidation/DnsDp.php | 45 + .../AcmeClient/LeValidation/DnsDreamhost.php | 44 + .../AcmeClient/LeValidation/DnsDuckdns.php | 44 + .../AcmeClient/LeValidation/DnsDyn.php | 46 + .../AcmeClient/LeValidation/DnsDynu.php | 45 + .../AcmeClient/LeValidation/DnsEuserv.php | 46 + .../AcmeClient/LeValidation/DnsFreedns.php | 45 + .../LeValidation/DnsGandiLivedns.php | 44 + .../AcmeClient/LeValidation/DnsGcloud.php | 88 + .../AcmeClient/LeValidation/DnsGd.php | 45 + .../AcmeClient/LeValidation/DnsGdnsdk.php | 45 + .../AcmeClient/LeValidation/DnsHe.php | 45 + .../AcmeClient/LeValidation/DnsHetzner.php | 44 + .../AcmeClient/LeValidation/DnsHostingde.php | 45 + .../AcmeClient/LeValidation/DnsInfoblox.php | 45 + .../AcmeClient/LeValidation/DnsInwx.php | 45 + .../AcmeClient/LeValidation/DnsIspconfig.php | 47 + .../AcmeClient/LeValidation/DnsJoker.php | 45 + .../AcmeClient/LeValidation/DnsKinghost.php | 45 + .../AcmeClient/LeValidation/DnsKnot.php | 45 + .../AcmeClient/LeValidation/DnsLeaseweb.php | 44 + .../AcmeClient/LeValidation/DnsLexicon.php | 55 + .../AcmeClient/LeValidation/DnsLinode.php | 46 + .../AcmeClient/LeValidation/DnsLinodeV4.php | 46 + .../AcmeClient/LeValidation/DnsLoopia.php | 46 + .../AcmeClient/LeValidation/DnsLua.php | 45 + .../AcmeClient/LeValidation/DnsMe.php | 45 + .../AcmeClient/LeValidation/DnsMiab.php | 46 + .../AcmeClient/LeValidation/DnsNamecheap.php | 51 + .../AcmeClient/LeValidation/DnsNamecom.php | 45 + .../AcmeClient/LeValidation/DnsNamesilo.php | 46 + .../AcmeClient/LeValidation/DnsNetcup.php | 48 + .../AcmeClient/LeValidation/DnsNsone.php | 44 + .../AcmeClient/LeValidation/DnsNsupdate.php | 52 + .../AcmeClient/LeValidation/DnsOpnsense.php | 54 + .../AcmeClient/LeValidation/DnsOvh.php | 47 + .../AcmeClient/LeValidation/DnsPdns.php | 46 + .../AcmeClient/LeValidation/DnsPleskxml.php | 46 + .../LeValidation/DnsSchlundtech.php | 45 + .../AcmeClient/LeValidation/DnsSelectel.php | 44 + .../AcmeClient/LeValidation/DnsServercow.php | 45 + .../AcmeClient/LeValidation/DnsUnoeuro.php | 45 + .../AcmeClient/LeValidation/DnsVariomedia.php | 44 + .../AcmeClient/LeValidation/DnsVscale.php | 44 + .../AcmeClient/LeValidation/DnsYandex.php | 44 + .../AcmeClient/LeValidation/DnsZilore.php | 44 + .../AcmeClient/LeValidation/DnsZonomi.php | 44 + .../AcmeClient/LeValidation/HttpOpnsense.php | 140 ++ .../AcmeClient/LeValidationFactory.php | 90 + .../AcmeClient/LeValidationInterface.php | 49 + .../models/OPNsense/AcmeClient/AcmeClient.xml | 18 +- .../models/OPNsense/AcmeClient/Menu/Menu.xml | 4 +- .../OPNsense/AcmeClient/Migrations/M2_0_0.php | 54 + .../views/OPNsense/AcmeClient/accounts.volt | 290 ++- .../OPNsense/AcmeClient/certificates.volt | 7 +- .../views/OPNsense/AcmeClient/settings.volt | 2 +- .../OPNsense/AcmeClient/validations.volt | 4 +- .../OPNsense/AcmeClient/certhelper.php | 1655 ----------------- .../scripts/OPNsense/AcmeClient/lecert.php | 193 ++ .../conf/actions.d/actions_acmeclient.conf | 22 +- 101 files changed, 6363 insertions(+), 1710 deletions(-) create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartHaproxy.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartNginx.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadHighwinds.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadSftp.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationInterface.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Dns1984hosting.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmeproxy.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAd.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAli.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArvan.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAutodns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAws.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCloudns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCn.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCx.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCyon.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDa.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDgon.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsimple.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDo.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDoapi.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDp.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDreamhost.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDuckdns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDyn.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynu.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEuserv.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFreedns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGd.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGdnsdk.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHe.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetzner.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHostingde.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInfoblox.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIspconfig.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJoker.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKinghost.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKnot.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLeaseweb.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLexicon.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinode.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinodeV4.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLoopia.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLua.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMe.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMiab.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecheap.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecom.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamesilo.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNetcup.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsone.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOpnsense.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOvh.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPdns.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPleskxml.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSchlundtech.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsServercow.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUnoeuro.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVariomedia.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVscale.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsYandex.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZilore.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZonomi.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php create mode 100644 security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationInterface.php create mode 100644 security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M2_0_0.php delete mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index 9c9d25f78..97a5889df 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.36 +PLUGIN_VERSION= 2.0 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon diff --git a/security/acme-client/pkg-descr b/security/acme-client/pkg-descr index 25a85e16d..ad11ff7de 100644 --- a/security/acme-client/pkg-descr +++ b/security/acme-client/pkg-descr @@ -4,3 +4,321 @@ and smallest and smartest shell script" to automatically issue and renew the free certificates from Let's Encrypt. WWW: https://github.com/acmesh-official/acme.sh + +Plugin Changelog +================ + +2.0 + +Added: +* add new OOP backend to improve reliability and maintainability (#1398) +* add status for accounts to backend and WebGUI +* add button to manually trigger account registration +* add plugin changelog + +Fixed: +* fix bug where configuration could get lost (#1526) +* fix Cyon DNS API (password not set) + +Changed: +* now an Automation may run multiple times during bulk issue/renewal (previously only once) +* rename "Validation Methods" to "Challenge Types" to adopt official LE wording +* rename Menu entry "Automation" to "Automations" +* specify python version for gcloud SDK +* rephrase several log messages +* add more detailed output when debug logging is enabled + +1.36 + +Added: +* add ability to rerun automations (#1962) + +1.35 + +Added: +* add support for Linode Cloud API (#1940) +* add support for 1984Hosting API (#1945) + +Changed: +* remove outdated bundled version of dns_opnsense.sh (#1888) + +1.34 + +Added: +* add support for dnsapi ArvanCloud (#1834) +* add support for dnsapi Hetzner (#1870) + +Changed: +* restore proper sorting in DNS API list + +1.33 + +Added: +* add NSUPDATE_ZONE support to nsupdate DNS-01 service (#1851) + +1.32 + +Added: +* add support for Acmeproxy DNS provider (#1838) + +Changed: +* improve support for dnsapi Euserv.eu (#1790) + +1.31 + +Added: +* add support for dnsapi SchlundTech (#1728) +* add support for dnsapi Euserv (#1779) +* add support for dnsapi Leaseweb (#1670) + +Changed: +* sftp export: make the "fullchain" filename configurable (#1776) + +1.30 + +Changed: +* update acme.sh GitHub link to new repo URL (#1744) + +1.29 + +Added: +* add support for CloudFlare token (#1625) +* add support for MailinaBox DNS API (#1531) +* add support for Plesk XML API (#1567) +* add support for Variomedia DNS API + +Fixed: +* fix IPv6 support for "automatic port forward" validation method (#1590) + +Changed: +* validate IPv4 and IPv6 addresses before using them for "automatic port forward" +* enable IPv6 support on local ACME webservice (when system.ipv6allow is enabled) + +1.28 + +Changed: +* correct minor spelling error (#1628) +* log filename not compatible with new log view (#1593) + +1.27 + +Added: +* add support for Loopia DNS API (#1529) +* automations can now restart Captive Portal or IPsec service after cert renewal (#1534) +* add support for 60+ DNS APIs through Lexicon (#1524) + +Fixed: +* don't break accounts when switching between stg/prod Let's Encrypt environments (#1528) + +Changed: +* add py-dns-lexicon as plugin dependency to support it in DNS-01 out-of-the-box +* support acme.sh debug log level 2 and 3 (#1546) + +1.26 + +Added: +* new automation: support cert upload via sftp (#1455) +* add support for OPNsense's BIND plugin (#1491) +* add support for DNS alias mode (#1492, #1301) + +Changed: +* add headers for certificate options for the sake of clarity + +1.25 + +Added: +* add support for netcup DNS API (#1350) + +Fixed: +* updating an existing cert in Highwinds API failed with a 404 error (wrong HTTP method) + +Changed: +* fix "Use of undefined constant" PHP errors +* treat certificate serial number as string not as integer +* move "remove certificate" button to the end of the button list + +1.24 + +Added: +* add support for Domain-Offensive LetsEncrypt API dns_doapi (#1294) +* add support for Namecheap API (dns_namecheap) +* add support for Google Cloud DNS API dns_gcloud (#549) +* run acme.sh --remove when a cert is removed from the GUI (#1380) +* add a new button to remove the private key (#990) + +Fixed: +* certificate status not correctly updated (#1307) + +Changed: +* add log message when certificate status is updated (refs #1307) + +1.23 + +Fixed: +* renewal interval is ignored (#1221) + +1.22 + +Added: +* support DNS-01 with hosting.de API (#1234) + +Changed: +* streamline log messages, use "AcmeClient" instead of "LE" + +1.21 + +Added: +* possible breaking change: the API endpoint to update individual certs/accounts/etc. has been renamed from "set" to "update" + +Fixed: +* bulk deleting does not work (#1163) + +Changed: +* migrate to mutable controller (required to fix #1163) + +1.20 + +Added: +* new button to reset all acme states, useful after importing a config backup to a new installation (#243) + +1.19 + +Added: +* new automation: automatically upload certificates to Highwinds CDN (proof-of-concept, support for other APIs possible) + +Changed: +* rename "Restart Actions" to "Automation" (the old name has always been rather clumsy) +* change "Automation" position in Menu (it's optional, the new position reflects this) + +1.18 + +Added: +* add support for GratisDNS.dk (#1042) +* add support for ACME DNS + +1.17 + +Fixed: +* fix OCSP always enabled (#794) +* fix acme operations when using multiple accounts (#789) + +1.16 + +Added: +* add support for OCSP Must Staple extension + +Fixed: +* fix ecc certs renewal bug + +1.15 + +Added: +* add support to multiple dns api providers (#712) + +Changed: +* mask passwords by using password fields (#707) + +1.14 + +Added: +* add support for ClouDNS (#574) + +1.13 + +Added: +* update acme.sh to 2.7.5 (#418) + +Changed: +* fix missing fields for several DNS providers (#481) + +1.12 + +Added: +* compatibility with HAProxy plugin version 2.0 (refs #330) + +Fixed: +* fix missing fields for Hurricane Electric (#334) + +1.11 + +Fixed: +* add missing field for DuckDNS (#287) + +1.9 + +Added: +* update acme.sh to version 2.7.2 (#210) +* add support for new DNS API hooks (#225) + +Fixed: +* Rename Certificate "Name" to "Common Name" for better clarity (#214) +* Fix title in "Renew" and "Revoke" dialogs +* Add dependency to BIND to fix nsupdate support +* fix 'Compilation failed: number too big' (#227) + +1.8 + +Added: +* drop bundled acme.sh in favour of the FreeBSD port + +Fixed: +* rename validation method "OPNsense Port Forward" to "OPNsense Web Service" to make it more clear that we're using an internal web service + +1.7 + +Fixed: +* fix $backend is not declared (#132) +* fix null exception in api + +1.6 + +Fixed: +* fix broken translation strings + +1.5 + +Fixed: +* try to solve disconnection issue (mostly during auto-renewal) (#109) +* try to fix "Node no longer exists" + +1.4 + +Changed: +* rename label "Validation Method" to "Challenge Type" + +1.3 + +Changed: +* remove support for custom restart actions (#100) +* avoid log message on missing restart action +* simplify JS code + +1.2 + +Fixed: +* properly import CA certificates (#84) +* don't make sensitive data world-readable + +Changed: +* hide params for restart actions when not selected +* remove prefixes from validation name +* hide http service entries when not selected +* log acme status for each cert + +1.1 + +Added: +* add HAProxy integration + +Fixed: +* avoid API exception when HAProxy integration is incomplete +* avoid error message if no restart action was specified +* do not run restart actions if cert was not changed + +Changed: +* add hide() trickery to hide entries when not selected +* relax fields validation (#70) + +1.0 + +Initial release (#6) diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php index b8bcc901a..ef6dfbf2d 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php @@ -33,6 +33,7 @@ namespace OPNsense\AcmeClient\Api; use OPNsense\Base\ApiMutableModelControllerBase; use OPNsense\Base\UIModelGrid; +use OPNsense\Core\Backend; use OPNsense\Core\Config; use OPNsense\AcmeClient\AcmeClient; @@ -73,6 +74,29 @@ class AccountsController extends ApiMutableModelControllerBase public function searchAction() { - return $this->searchBase('accounts.account', array('enabled', 'name', 'email'), 'name'); + return $this->searchBase('accounts.account', array('enabled', 'name', 'email', 'statusCode', 'statusLastUpdate'), 'name'); + } + + /** + * register account by uuid + * @param $uuid item unique id + * @return array status + */ + public function registerAction($uuid) + { + $result = array("result" => "failed"); + if ($this->request->isPost()) { + $mdlAcme = new AcmeClient(); + + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('accounts.account.' . $uuid); + if ($node != null) { + $backend = new Backend(); + $response = $backend->configdRun("acmeclient register-account ${uuid}"); + return array("response" => $response); + } + } + } + return $result; } } 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 0159e4631..5a56a56a0 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 @@ -69,9 +69,11 @@ class CertificatesController extends ApiMutableModelControllerBase 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}"); + $response = $backend->configdRun("acmeclient remove-cert {$uuid}"); + // Give configd some time to start this operation before the + // cert is removed from config. + sleep(2); } } return $this->delBase('certificates.certificate', $uuid); @@ -101,9 +103,8 @@ class CertificatesController extends ApiMutableModelControllerBase if ($uuid != null) { $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); if ($node != null) { - $cert_id = $node->id; $backend = new Backend(); - $response = $backend->configdRun("acmeclient sign-cert {$cert_id}"); + $response = $backend->configdRun("acmeclient sign-cert ${uuid}"); return array("response" => $response); } } @@ -123,9 +124,8 @@ class CertificatesController extends ApiMutableModelControllerBase 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}"); + $response = $backend->configdRun("acmeclient remove-key ${uuid}"); } } return $result; @@ -145,9 +145,8 @@ class CertificatesController extends ApiMutableModelControllerBase if ($uuid != null) { $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); if ($node != null) { - $cert_id = $node->id; $backend = new Backend(); - $response = $backend->configdRun("acmeclient revoke-cert {$cert_id}"); + $response = $backend->configdRun("acmeclient revoke-cert ${uuid}"); return array("response" => $response); } } @@ -167,9 +166,8 @@ class CertificatesController extends ApiMutableModelControllerBase if ($uuid != null) { $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); if ($node != null) { - $cert_id = $node->id; $backend = new Backend(); - $response = $backend->configdRun("acmeclient run-automation {$cert_id}"); + $response = $backend->configdRun("acmeclient run-automation ${uuid}"); } } return $result; diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php index 23264d2b5..d7de8f5f3 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php @@ -182,7 +182,6 @@ class ServiceController extends ApiControllerBase // finally run the syntax check $response = $backend->configdRun("acmeclient configtest"); return array("result" => $response); - // TODO: We may also want to check for duplicate cert names, etc. } /** diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php index f093bb840..3dcd37d54 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php @@ -384,7 +384,7 @@ class SettingsController extends ApiMutableModelControllerBase } /** - * Check wether the Google Cloud plugin is installed. + * Check whether the Google Cloud plugin is installed. * @return array status action */ public function getGcloudPluginStatusAction() @@ -402,7 +402,7 @@ class SettingsController extends ApiMutableModelControllerBase } /** - * Check wether the BIND plugin is installed. + * Check whether the BIND plugin is installed. * @return array status action */ public function getBindPluginStatusAction() diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml index 303b5e561..0fd183462 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml @@ -42,9 +42,9 @@ certificate.validationMethod - + dropdown - + certificate.autoRenewal diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml index 242f42010..28cee3b6c 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml @@ -9,19 +9,19 @@ acmeclient.settings.autoRenewal checkbox - + acmeclient.settings.environment dropdown - relaxed rate limits.
NOTE:Certificates signed by the staging environment are NOT valid. You need to forcefully re-sign (or delete and re-create) them after switching from staging to production environment.
]]>
+ relaxed rate limits.
NOTE:Certificates signed by the staging environment are NOT valid. You need to forcefully re-sign (or delete and re-create) them after switching from staging to production environment.
]]>
acmeclient.settings.haproxyIntegration checkbox - Requires that the OPNsense HAProxy plugin is installed. This will automatically add the required backend, server, action and ACL for you. You just need to select your HAProxy frontend when configuration the certificate or validation method.
NOTE:This will only work for HTTP-01 validation and HAProxy frontends running in http mode; TCP frontends are not supported.
]]>
+ Requires that the OPNsense HAProxy plugin is installed. This will automatically add the required backend, server, action and ACL for you. You just need to select your HAProxy frontend when configuration the certificate or challenge type.
NOTE:This will only work for HTTP-01 validation and HAProxy frontends running in http mode; TCP frontends are not supported.
]]>
acmeclient.settings.logLevel @@ -33,7 +33,7 @@ acmeclient.settings.challengePort text - + true diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php new file mode 100644 index 000000000..4f13e67a9 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php @@ -0,0 +1,263 @@ +uuid = $uuid; + + // Get config object + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + // Set log level + $this->setLoglevel(); + + // Set Let's Encrypt environment + $this->setEnvironment(); + + // Store acme filenames + $this->acme_args[] = '--home ' . self::ACME_HOME_DIR; + } + + /** + * generate private key and ACME config for this account + */ + public function generateKey() + { + // Collect account information + $account_conf_dir = self::ACME_BASE_ACCOUNT_DIR . '/' . (string)$this->config->id . '_' . $this->environment; + $account_conf_file = $account_conf_dir . '/account.conf'; + $account_key_file = $account_conf_dir . '/account.key'; + $account_json_file = $account_conf_dir . '/account.json'; + $account_ca_file = $account_conf_dir . '/ca.conf'; + $acme_conf = array(); + $acme_conf[] = "CERT_HOME='" . self::ACME_HOME_DIR . "'"; + $acme_conf[] = "LOG_FILE='" . self::ACME_LOG_FILE . "'"; + $acme_conf[] = "ACCOUNT_KEY_PATH='" . $account_key_file . "'"; + $acme_conf[] = "ACCOUNT_JSON_PATH='" . $account_json_file . "'"; + $acme_conf[] = "CA_CONF='" . $account_ca_file . "'"; + if (!empty((string)$this->config->email)) { + $acme_conf[] = "ACCOUNT_EMAIL='" . (string)$this->config->email . "'"; + } + + // Store some values for later re-use + $this->account_conf_file = $account_conf_file; + + // Create account configuration file + if (!is_dir($account_conf_dir)) { + mkdir($account_conf_dir, 0700, true); + } + file_put_contents($account_conf_file, (string)implode("\n", $acme_conf) . "\n"); + chmod($account_conf_file, 0600); + + // Check if account key already exists both in filesystem and in config + if (!is_file($account_key_file) || empty((string)$this->config->key)) { + LeUtils::log_debug('creating account key for ' . (string)$this->config->name, $this->debug); + + // Check if we have an account key in our configuration + if (!empty((string)$this->config->key)) { + LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name, $this->debug); + // Write key to disk + file_put_contents($account_key_file, (string)base64_decode((string)$this->config->key)); + chmod($account_key_file, 0600); + return true; + } else { + LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name, $this->debug); + // Preparation to run acme client + $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env['PATH'] = $this::ACME_ENV_PATH; + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + + // Run acme client to generate a account key + $acmecmd = '/usr/local/sbin/acme.sh ' + . '--createAccountKey ' + . implode(' ', $this->acme_args) . ' ' + . '--accountkeylength ' . self::ACME_ACCOUNT_KEY_LENGTH . ' ' + . "--accountconf ${account_conf_file}"; + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + } else { + LeUtils::log_error('unable to start acme client process'); + $this->setStatus(500); + return false; + } + + // Check exit code + if ($result) { + LeUtils::log_error('failed to create a new account key for ' . (string)$this->config->name); + $this->setStatus(300); + return false; + } + + // Read account key file + $account_key_content = @file_get_contents($account_key_file); + if (empty($account_key_content) || ($account_key_content == false)) { + LeUtils::log_error("unable to read account key from file ${account_key_file}"); + $this->setStatus(500); + return false; + } + + // Reload to get most recent config + Config::getInstance()->forceReload(); + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + // Import account key into config + $this->config->key = base64_encode($account_key_content); + + // Serialize to config and save + $this->model->serializeToConfig(); + Config::getInstance()->save(); + + // Refresh config objects + Config::getInstance()->forceReload(); + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + if (empty((string)$this->config->key)) { + $this->setStatus(500); + LeUtils::log_error('failed to save account key for ' . (string)$this->config->name); + return false; + } + LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name, $this->debug); + return true; + } + } + return true; + } + + /** + * check if account is already registered + * @return bool + */ + public function isRegistered() + { + if (!empty((string)$this->config->statusLastUpdate) and !empty((string)$this->config->key) and ((string)$this->config->statusCode == '200')) { + return true; + } + return false; + } + + /** + * register account with Let's Encrypt + * @return bool + */ + public function register() + { + if (!($this->isEnabled())) { + LeUtils::log('ignoring disabled account: ' . (string)$this->config->name); + return false; + } + + // Make sure a private already exists + if (!($this->generateKey())) { + LeUtils::log_error('aborting registration due to issues with account key: ' . (string)$this->config->name); + return false; + } + + // Check if account is already registered + if (!($this->isRegistered())) { + LeUtils::log_debug('starting account registration for ' . (string)$this->config->name, $this->debug); + + // Preparation to run acme client + $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env['PATH'] = $this::ACME_ENV_PATH; + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + + // Run acme client + $acmecmd = '/usr/local/sbin/acme.sh ' + . '--registeraccount ' + . implode(' ', $this->acme_args) . ' ' + . '--accountconf ' . $this->account_conf_file; + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + } else { + LeUtils::log_error('unable to start acme client process'); + $this->setStatus(500); + return false; + } + + // Check validation result + if ($result) { + LeUtils::log_error('account registration failed for ' . $this->config->name); + $this->setStatus(400); + return false; + } + + // Update account status. + LeUtils::log_error('account registration successful for ' . $this->config->name); + $this->setStatus(200); + } else { + LeUtils::log_debug('account already registered: ' . (string)$this->config->name, $this->debug); + } + + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php new file mode 100644 index 000000000..cb4ad40f1 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php @@ -0,0 +1,97 @@ + + * 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. + */ + +namespace OPNsense\AcmeClient\LeAutomation; + +use OPNsense\Core\Backend; +use OPNsense\Core\Config; +use OPNsense\AcmeClient\LeAccount; +use OPNsense\AcmeClient\LeUtils; + +/** + * LeAutomation stub file, contains shared logic for all automations. + * @package OPNsense\AcmeClient + */ +abstract class Base extends \OPNsense\AcmeClient\LeCommon +{ + public const CONFIG_PATH = 'actions.action'; + + /** + * Initialize LeAutomation object by adding the required configuration. + * @return boolean + */ + public function init(string $certid, string $accountuuid) + { + // Get config object + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + // Get account object to query ID + $account = new LeAccount($accountuuid); + + // Store auxiliary information (required to glue stuff together) + $this->cert_id = $certid; + $this->account_id = (string)$account->id; + $this->account_uuid = (string)$account->uuid; + + // Set log level + $this->setLoglevel(); + + // Set Let's Encrypt environment + $this->setEnvironment(); + + return true; + } + + /** + * run all tasks related to this automation + * @return boolean + */ + public function run() + { + if (!($this->isEnabled())) { + LeUtils::log('ignoring disabled automation: ' . (string)$this->config->name); + return true; // not an error + } + + LeUtils::log('running automation: ' . $this->config->name); + $backend = new \OPNsense\Core\Backend(); + $response = $backend->configdRun((string)$this->command, $this->command_args); + return true; + } + + /** + * get automation type from configuration + * @return string + */ + public function getType() + { + return $this->config->type; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php new file mode 100644 index 000000000..810fafab5 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php @@ -0,0 +1,51 @@ +config->configd)) { + LeUtils::log_error('no configd command specified for automation: ' . $this->config->name); + return false; + } + + $this->command = (string)$this->config->configd; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php new file mode 100644 index 000000000..e63cf479e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php @@ -0,0 +1,45 @@ +command = 'webgui restart 2'; + $this->command_args = true; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartHaproxy.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartHaproxy.php new file mode 100644 index 000000000..c40e9fce7 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartHaproxy.php @@ -0,0 +1,44 @@ +command = 'haproxy restart'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartNginx.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartNginx.php new file mode 100644 index 000000000..fc803e78a --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartNginx.php @@ -0,0 +1,44 @@ +command = 'nginx restart'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadHighwinds.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadHighwinds.php new file mode 100644 index 000000000..1e9bfdcba --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadHighwinds.php @@ -0,0 +1,45 @@ +cert_id . ' ' . $this->config->id; + $this->command = $command; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadSftp.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadSftp.php new file mode 100644 index 000000000..11770c8f5 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/UploadSftp.php @@ -0,0 +1,45 @@ +cert_id . ' ' . $this->config->id; + $this->command = $command; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php new file mode 100644 index 000000000..5c6b53177 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php @@ -0,0 +1,81 @@ +getNodeByReference(self::CONFIG_PATH . '.' . $uuid); + if ($obj == null) { + LeUtils::log_error("automation not found: ${uuid}"); + return null; + } + + // Convert to PascalCase, required to find the class name. + $auto_name = str_replace(' ', '', ucwords(str_replace(array('-', '_'), ' ', (string)$obj->type))); + + // Search class name + foreach (glob(__DIR__ . "/LeAutomation/*.php") as $filename) { + $file_found = basename($filename, '.php'); + try { + $reflClass = new \ReflectionClass("OPNsense\\AcmeClient\\LeAutomation\\{$file_found}"); + } catch (\ReflectionException $e) { + break; + } + if ($reflClass->implementsInterface('OPNsense\\AcmeClient\\LeAutomationInterface')) { + if ($file_found == $auto_name) { + // Create new object + $objAuto = $reflClass->newInstance(); + $objAuto->setUuid($uuid); + return $objAuto; + } + } + } + + LeUtils::log_error("automation not supported: " . (string)$obj->type . " (${uuid})"); + return null; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationInterface.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationInterface.php new file mode 100644 index 000000000..6fb9b1f88 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationInterface.php @@ -0,0 +1,43 @@ +uuid = $uuid; + $this->force = $force; + + // Get config object + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + // Get account object to query ID + $account = new LeAccount((string)$this->config->account); + if (empty($account) || $account == null) { + LeUtils::log_error('unable to load account information: ' . (string)$this->config->account); + return false; + } + + // Store auxiliary information (required to glue stuff together) + $this->account_id = (string)$account->getId(); + $this->account_uuid = (string)$account->getUuid(); + + // Set log level + $this->setLoglevel(); + + // Set Let's Encrypt environment + $this->setEnvironment(); + + // Handle special key types + if ($this->config->keyLength == 'key_ec256' || $this->config->keyLength == 'key_ec384') { + // Pass --ecc to acme client to locate the correct cert directory + $this->acme_args[] = '--ecc'; + } + + // Store cert filenames + $this->cert_file = (string)sprintf(self::ACME_CERT_FILE, $this->config->id); + $this->cert_key_file = (string)sprintf(self::ACME_KEY_FILE, $this->config->id); + $this->cert_chain_file = (string)sprintf(self::ACME_CHAIN_FILE, $this->config->id); + $this->cert_fullchain_file = (string)sprintf(self::ACME_FULLCHAIN_FILE, $this->config->id); + + // Store acme filenames + $this->acme_args[] = '--home ' . self::ACME_HOME_DIR; + $this->acme_args[] = '--certpath ' . $this->cert_file; + $this->acme_args[] = '--keypath ' . $this->cert_key_file; + $this->acme_args[] = '--capath ' . $this->cert_chain_file; + $this->acme_args[] = '--fullchainpath ' . $this->cert_fullchain_file; + } + + /** + * Import the certificate into OPNsense's trust storage. + * @param bool $skip_validation try to import even if some checks fail + * @return bool + */ + public function import(bool $skip_validation = false) + { + if (!($this->isEnabled())) { + LeUtils::log("ignoring disabled certificate: " . (string)$this->config->name); + return false; + } + + // Cannot import if certificate was not issued yet. + // NOTE: When the certificate was just issued, then the cert status + // does not reflect this yet and must be ignored by setting $skip_validation. + if (!($this->isIssued()) && !($skip_validation)) { + LeUtils::log('ignoring import request for certificate ' . (string)$this->config->name . ' (not issued or revoked)'); + return false; + } + + // Reload to get most recent config + Config::getInstance()->forceReload(); + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + // Check if certificate files can be found + clearstatcache(); // don't let the cache fool us + foreach (array($this->cert_file, $this->cert_key_file, $this->cert_chain_file, $this->cert_fullchain_file) as $file) { + if (!is_file($file)) { + LeUtils::log_error("unable to import certificate " . $this->config->name . ", file not found: ${file}"); + Config::getInstance()->unlock(); + return false; + } + } + + /** + * Step 1: import CA + */ + + // Read contents from CA file + $ca_content = @file_get_contents($this->cert_chain_file); + if ($ca_content != false) { + $ca_subject = cert_get_subject($ca_content, false); + $ca_serial = cert_get_serial($ca_content, false); + $ca_cn = LeUtils::local_cert_get_cn($ca_content, false); + $ca_issuer = cert_get_issuer($ca_content, false); + $ca_purpose = cert_get_purpose($ca_content, false); + } else { + LeUtils::log_error('unable to read CA certificate content from file'); + Config::getInstance()->unlock(); + return false; + } + + // Prepare CA for import in Cert Manager + $ca = array(); + $ca['crt'] = base64_encode($ca_content); + $ca['refid'] = uniqid(); + $ca_found = false; + + // Check if CA was previously imported + foreach (Config::getInstance()->object()->ca as $cacrt) { + $cacrt_subject = cert_get_subject($cacrt->crt, true); + $cacrt_issuer = cert_get_issuer($cacrt->crt, true); + if (($ca_subject == $cacrt_subject) and ($ca_issuer == $cacrt_issuer)) { + // Use old refid instead of generating a new one + $ca['refid'] = (string)$cacrt->refid; + $ca_found = true; + break; + } + } + + // Collect required CA information + $ca_cn = LeUtils::local_cert_get_cn($ca_content, false); + $ca['descr'] = (string)$ca_cn . ' (Let\'s Encrypt)'; + + // Prepare CA for import + LeUtils::local_ca_import($ca, $ca_content); + + // Check if CA was found in config + if ($ca_found == true) { + // Update existing CA + foreach (Config::getInstance()->object()->ca as $cacrt) { + if ((string)$cacrt->refid == $ca['refid']) { + $cacrt->crt = $ca['crt']; + $cacrt->descr = $ca['descr']; + break; + } + } + } else { + // Create new CA + LeUtils::log("importing Let's Encrypt CA: ${ca_cn}"); + $newca = Config::getInstance()->object()->addChild('ca'); + foreach (array_keys($ca) as $cacfg) { + $newca->addChild($cacfg, (string)$ca[$cacfg]); + } + } + + /** + * Step 2: import certificate + */ + + // Read contents from certificate file + $cert_content = @file_get_contents($this->cert_file); + if ($cert_content != false) { + $cert_subject = cert_get_subject($cert_content, false); + $cert_serial = cert_get_serial($cert_content, false); + $cert_cn = LeUtils::local_cert_get_cn($cert_content, false); + $cert_issuer = cert_get_issuer($cert_content, false); + $cert_purpose = cert_get_purpose($cert_content, false); + } else { + LeUtils::log_error('unable to read certificate content from file'); + Config::getInstance()->unlock(); + $this->setStatus(500); + return false; + } + + // Prepare certificate for import in Cert Manager + $cert = array(); + $cert_refid = uniqid(); + $cert['refid'] = $cert_refid; + $cert['caref'] = (string)$ca['refid']; + $import_log_message = 'imported'; + $cert_found = false; + + // Check if cert was previously imported + if (!empty((string)$this->config->certRefId)) { + // Check if the previously imported certificate can still be found + foreach (Config::getInstance()->object()->ca as $cfgCert) { + // Check if IDs match + if ((string)$this->config->certRefId == (string)$cfgCert->refid) { + $cert_found = true; + break; + } + } + // Existing cert? + if ($cert_found) { + // Use old refid instead of generating a new one + $cert_refid = (string)$this->config->certRefId; + $import_log_message = 'updated'; + } + } else { + // Not found. Just import as new cert. + } + + // Read private key + $key_content = @file_get_contents($this->cert_key_file); + if ($key_content == false) { + LeUtils::log_error('unable to read private key from file: ' . $this->cert_key_file); + Config::getInstance()->unlock(); + $this->setStatus(500); + return false; + } + + // Collect required cert information + $cert_cn = LeUtils::local_cert_get_cn($cert_content, false); + $cert['descr'] = (string)$cert_cn . ' (Let\'s Encrypt)'; + $cert['refid'] = $cert_refid; + + // Prepare certificate for import + cert_import($cert, $cert_content, $key_content); + + // Check if cert was found in config + if ($cert_found == true) { + // Update existing cert + foreach (Config::getInstance()->object()->cert as $cfgCert) { + if ((string)$cfgCert->refid == $cert['refid']) { + $cfgCert->crt = $cert['crt']; + $cfgCert->prv = $cert['prv']; + $cfgCert->descr = $cert['descr']; + break; + } + } + } else { + // Create new cert + $newcert = Config::getInstance()->object()->addChild('cert'); + foreach (array_keys($cert) as $certcfg) { + $newcert->addChild($certcfg, (string)$cert[$certcfg]); + } + } + LeUtils::log("${import_log_message} Let's Encrypt X.509 certificate: ${cert_cn}"); + + /** + * Step 3: update configuration + */ + + // Add refid to certObj + $this->config->certRefId = $cert_refid; + // Set update/create time + $this->config->lastUpdate = time(); + + // Serialize to config and save + $this->model->serializeToConfig(); + Config::getInstance()->save(); + + // Reload to get most recent config + Config::getInstance()->forceReload(); + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + return true; + } + + /** + * check if certificate is already issued by Let's Encrypt + * @return bool + */ + public function isIssued() + { + return (string)$this->config->statusCode == 200 ? true : false; + } + + /** + * issue or renew the certificate + * @return bool + */ + public function issue() + { + if (!($this->isEnabled())) { + LeUtils::log('ignoring disabled certificate: ' . (string)$this->config->name); + return false; + } + + // Issue or renew? + if (!empty((string)$this->config->lastUpdate) and !($this->force)) { + $acme_action = "renew"; + $renew = true; + } else { + // Default: Issue a new certificate. + // If "force" is specified, forcefully re-issue the cert, no matter if it's required. + // NOTE: This is useful when switching from acme staging to production servers. + $acme_action = "issue"; + $renew = false; + } + + // Decide whether or not to continue. + if (!($this->needsRenewal()) and !($this->force)) { + // Renewal not required. Do nothing. + LeUtils::log("issue/renewal not required for certificate: " . (string)$this->config->name); + return false; + } + LeUtils::log("${acme_action} certificate: " . (string)$this->config->name); + + // Ensure that account is registered. + if (!($this->setAccount())) { + return false; + } + + // Setup ACME environment for this certificate. + $certdir = (string)sprintf(self::ACME_CERT_DIR, (string)$this->config->id); + $keydir = (string)sprintf(self::ACME_KEY_DIR, (string)$this->config->id); + $configdir = (string)sprintf(self::ACME_CONFIG_DIR, (string)$this->config->id); + foreach (array($certdir, $keydir, $configdir) as $dir) { + if (!is_dir($dir)) { + LeUtils::log_debug("creating directory: ${dir}", $this->debug); + mkdir($dir, 0700, true); + } + } + + // Perform preparation tasks + if (!($this->setValidation())) { + $this->setStatus(300); + return false; // validation method is invalid + } + + // Let's start certificate validation... + if ($this->validation->run($renew)) { + LeUtils::log('successfully issued/renewed certificate: ' . (string)$this->config->name); + } else { + LeUtils::log_error('validation for certificate failed: ' . (string)$this->config->name); + $this->setStatus(400); + return false; + } + + // Import certificate. + if (!($this->import(true))) { + LeUtils::log_error('failed to import certificate: ' . (string)$this->config->name); + $this->setStatus(500); + return false; + } + + // Run referenced automations. + $this->runAutomations(); + + // Update cert status. + $this->setStatus(200); + + return true; + } + + /** + * calculate next renewal date for this certificate + * @return bool + */ + public function needsRenewal() + { + $return = false; + + // Collect required information + $last_update = !empty((string)$this->config->lastUpdate) ? (string)$this->config->lastUpdate : 0; + $current_time = new \DateTime(); + $last_update_time = new \DateTime(); + $last_update_time->setTimestamp($last_update); + $renew_interval = (string)$this->config->renewInterval; + $next_update = $last_update_time->add(new \DateInterval('P' . $renew_interval . 'D')); + + // Do the math + if ($current_time >= $next_update) { + LeUtils::log('certificate must be issued/renewed: ' . (string)$this->config->name); + $return = true; + } + + return $return; + } + + /** + * completely remove the certificate and all related configuration from filesystem + * @return bool + */ + public function remove() + { + // NOTE: + // Removal is allowed even if the cert is disabled. + + // Cannot remove if certificate was not issued yet. + if (empty((string)$this->config->lastUpdate)) { + LeUtils::log('ignoring removal request for certificate ' . (string)$this->config->name . ' (not issued yet)'); + return false; + } + LeUtils::log('wiping certificate config: ' . (string)$this->config->name); + + // Preparation to run acme client + $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env['PATH'] = $this::ACME_ENV_PATH; + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + + // Run acme client to remove certificate and related config + $acmecmd = '/usr/local/sbin/acme.sh ' + . '--remove ' + . implode(' ', $this->acme_args) . ' ' + . '--domain ' . (string)$this->config->name; + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + } else { + LeUtils::log_error('unable to start acme client process'); + return false; + } + + // Check exit code + if ($result) { + LeUtils::log_error('error removing certificate ' . (string)$this->config->name); + return false; + } + + // Remove all certificate files (just to be sure) + // NOTE: This also resets the cert status. + $this->reset(); + + return true; + } + + /** + * reset the certificate by removing only it's private key and the signed certificate + * @return bool + */ + public function reset() + { + // NOTE: Reset is allowed even if the cert is disabled. + LeUtils::log('removing certificate files: ' . (string)$this->config->name); + $cert_files = [ + $this->cert_file, + $this->cert_key_file, + $this->cert_chain_file, + $this->cert_fullchain_file, + ]; + foreach ($cert_files as $_file) { + if (file_exists($_file)) { + unlink($_file); + } + } + + // Reset cert status + $this->setStatus(100); + return true; + } + + /** + * revoke the certificate + * @return bool + */ + public function revoke() + { + // NOTE: Revocation is allowed even if the cert is disabled. + + // Revocation will fail if additional domain names were added + // to the certificate after issue/renewal. + + // Cannot revoke if certificate was not issued yet. + if (!($this->isIssued())) { + LeUtils::log('ignoring revocation request for certificate ' . (string)$this->config->name . ' (not issued yet)'); + return false; + } + LeUtils::log('revoking certificate: ' . (string)$this->config->name); + + // Collect account information + $account_conf_dir = self::ACME_BASE_ACCOUNT_DIR . '/' . $this->account_id . '_' . $this->environment; + $account_conf_file = $account_conf_dir . '/account.conf'; + + // Preparation to run acme client + $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env['PATH'] = $this::ACME_ENV_PATH; + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + + // Run acme client to revoke certificate + $acmecmd = '/usr/local/sbin/acme.sh ' + . '--revoke ' + . implode(' ', $this->acme_args) . ' ' + . '--domain ' . (string)$this->config->name . ' ' + . "--accountconf ${account_conf_file}"; + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + } else { + LeUtils::log_error('unable to start acme client process'); + return false; + } + + // Check exit code + if ($result) { + LeUtils::log_error('failed to revoke certificate ' . (string)$this->config->name); + $this->setStatus(400); + return false; + } + LeUtils::log('successfully revoked certificate: ' . (string)$this->config->name); + + // Reset cert status + $this->setStatus(250); + return true; + } + + /** + * run all automations for this certificate + * @return bool + */ + public function runAutomations() + { + if (!($this->isEnabled())) { + LeUtils::log('ignoring disabled certificate: ' . (string)$this->config->name); + return false; + } + + // Check if any automations are configured for this cert + if (empty((string)$this->config->restartActions)) { + return true; // no automations, no error + } + + // Walk through all linked automations. + LeUtils::log('running automations for certificate: ' . (string)$this->config->name); + $automations = explode(',', (string)$this->config->restartActions); + foreach ($automations as $auto_uuid) { + $autoFactory = new LeAutomationFactory(); + $automation = $autoFactory->getAutomation($auto_uuid); + $automation->init($this->getId(), (string)$this->config->account); + // Ignore invalid automations. + if ($automation->prepare()) { + $automation->run(); + } + } + + return true; + } + + /** + * configure and register the referenced account + * @return bool + */ + public function setAccount() + { + // Ensure that account is registered. + $account = new LeAccount((string)$this->config->account); + if (empty($account)) { + $this->setStatus(300); // update cert status + return false; // account invalid or it was deleted + } elseif (!($account->isRegistered())) { + $account->generateKey(); + if (!($account->register())) { + $this->setStatus(400); // update cert status + return false; // account registration failed + } + // Refresh config objects, account may have modified the configuration. + Config::getInstance()->forceReload(); + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + } + LeUtils::log('account is registered: ' . (string)$account->config->name); + return true; + } + + /** + * configure the validation method + * @return bool + */ + public function setValidation() + { + if (empty((string)$this->validation)) { + // Setup new validation object + $valFactory = new LeValidationFactory(); + $val = $valFactory->getValidation((string)$this->config->validationMethod); + if (!isset($val) or empty($val)) { + LeUtils::log_error('invalid challenge type for certificate: ' . (string)$this->config->name); + return false; + } + if (!$val->init((string)$this->config->id, (string)$this->config->account)) { + LeUtils::log_error('failed to initialize validation for certificate: ' . (string)$this->config->name); + return false; + } + + // Configure validation object + $val->setNames($this->config->name, $this->config->altNames); + $val->setRenewal((int)$this->config->renewInterval); + $val->setForce($this->force); + // strip prefix from key value + $val->setKey(substr($this->config->keyLength, 4)); + $val->prepare(); + + // Store validation object + $this->validation = $val; + } + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php new file mode 100644 index 000000000..3ae1da021 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php @@ -0,0 +1,214 @@ +config->id; + } + + /** + * get UUID from auxiliary configuration object + * @return string + */ + public function getUuid() + { + return (string)$this->config->uuid; + } + + /** + * load config object from configuration + * @return bool + */ + public function loadConfig(string $path, string $uuid) + { + // Get config object + $model = new \OPNsense\AcmeClient\AcmeClient(); + $obj = $model->getNodeByReference("${path}.${uuid}"); + if ($obj == null) { + LeUtils::log_error("config of type ${path} not found: ${uuid}"); + return false; + } + // Store config objects + $this->config = $obj; + $this->model = $model; + return true; + } + + /** + * check if object is enabled in configuration + * @return bool + */ + public function isEnabled() + { + return (string)$this->config->enabled == 1 ? true : false; + } + + /** + * set Let's Encrypt environment for acme.sh + */ + public function setEnvironment() + { + $this->environment = (string)$this->model->getNodeByReference('settings.environment'); + $this->acme_args[] = $this->environment == 'stg' ? '--staging' : null; + } + + /** + * set log level for acme.sh and configure optional debug logging + */ + public function setLoglevel() + { + $loglevel = (string)$this->model->getNodeByReference('settings.logLevel'); + + switch ($loglevel) { + case 'extended': + $this->acme_args[] = '--log-level 2'; + $this->debug = false; + break; + case 'debug': + $this->acme_args[] = '--debug'; + $this->debug = true; + break; + case 'debug2': + $this->acme_args[] = '--debug 2'; + $this->debug = true; + break; + case 'debug3': + $this->acme_args[] = '--debug 3'; + $this->debug = true; + break; + default: + $this->acme_args[] = '--log-level 1'; + $this->debug = false; + break; + } + } + + /** + * update status information to reflect the result of the last operation + * Supported status codes are: + * 100 pending + * 200 cert issued / acct registered + * 250 cert revoked / acct deactivated + * 300 configuration error + * 400 issue/renew/registration failed + * 500 internal error (code issues, bad luck, unexpected errors, ...) + * Feel free to add more status codes to support new use-cases. + * @return bool + */ + public function setStatus(int $statusCode) + { + // Update attributes. + $this->config->statusCode = $statusCode; + $this->config->statusLastUpdate = time(); + + // Serialize to config and save + Config::getInstance()->unlock(); + $this->model->serializeToConfig(); + Config::getInstance()->save(); + + // Reload to get most recent config + Config::getInstance()->forceReload(); + $this->loadConfig($this::CONFIG_PATH, $this->uuid); + + return true; + } + + /** + * set UUID of auxiliary configuration object + */ + public function setUuid(string $uuid) + { + $this->uuid = $uuid; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php new file mode 100644 index 000000000..dd5ce0ff3 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php @@ -0,0 +1,189 @@ + + * Copyright (C) 2008 Shrew Soft Inc. + * 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. + */ + +namespace OPNsense\AcmeClient; + +use OPNsense\Core\Config; + +/** + * Helper functions for LeAcme + * @package OPNsense\AcmeClient + */ +class LeUtils +{ + public static function base64url_decode($str) + { + return base64_decode(str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT)); + } + + public static function base64url_encode($str) + { + return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); + } + + // Copied from system_camanager.php. + public static function local_ca_import(&$ca, $str, $key = "", $serial = 0) + { + // Get config object. + $config = Config::getInstance()->object(); + + $ca['crt'] = base64_encode($str); + if (!empty($key)) { + $ca['prv'] = base64_encode($key); + } + if (!empty($serial)) { + $ca['serial'] = $serial; + } + $subject = cert_get_subject($str, false); + $issuer = cert_get_issuer($str, false); + + // Find my issuer unless self-signed + if ($issuer != $subject) { + $issuer_crt =& lookup_ca_by_subject($issuer); + if ($issuer_crt) { + $ca['caref'] = $issuer_crt['refid']; + } + } + + /* Correct if child certificate was loaded first */ + if (is_array($config['ca'])) { + foreach ($config['ca'] as & $oca) { + $issuer = cert_get_issuer($oca['crt']); + if ($ca['refid'] != $oca['refid'] && $issuer == $subject) { + $oca['caref'] = $ca['refid']; + } + } + } + if (is_array($config['cert'])) { + foreach ($config['cert'] as & $cert) { + $issuer = cert_get_issuer($cert['crt']); + if ($issuer == $subject) { + $cert['caref'] = $ca['refid']; + } + } + } + return true; + } + + // copied from certs.inc + public static function local_cert_get_cn($crt, $decode = true) + { + $sub = self::local_cert_get_subject_array($crt, $decode); + if (is_array($sub)) { + foreach ($sub as $s) { + if (strtoupper($s['a']) == "CN") { + return $s['v']; + } + } + } + return ""; + } + + // copied from certs.inc + public static function local_cert_get_subject_array($str_crt, $decode = true) + { + if ($decode) { + $str_crt = base64_decode($str_crt); + } + $inf_crt = openssl_x509_parse($str_crt); + $components = $inf_crt['subject']; + + if (!is_array($components)) { + return; + } + + $subject_array = array(); + + foreach ($components as $a => $v) { + $subject_array[] = array('a' => $a, 'v' => $v); + } + + return $subject_array; + } + + /** + * log runtime information + */ + public static function log($msg) + { + syslog(LOG_NOTICE, "AcmeClient: ${msg}"); + } + + /** + * log additional debug output + */ + public static function log_debug($msg, bool $debug = false) + { + if ($debug) { + syslog(LOG_NOTICE, "AcmeClient: ${msg}"); + } + } + + /** + * log error messages + */ + public static function log_error($msg) + { + syslog(LOG_ERR, "AcmeClient: ${msg}"); + } + + /** + * run arbitrary shell commands and log the result + * @param $proc_cmd string the command that should be run + * @param $proc_env array optional environment variables that should be used + * @return bool + */ + public static function run_shell_command($proc_cmd, $proc_env = array()) + { + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + $proc = proc_open($proc_cmd, $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + log_error(sprintf("AcmeClient: The shell command '%s' returned exit code '%d'", $proc_cmd, $result)); + return($result); + } else { + log_error(sprintf("AcmeClient: Unable to prepare shell command '%s'", $proc_cmd)); + return false; + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php new file mode 100644 index 000000000..d47d5e73c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php @@ -0,0 +1,298 @@ + + * 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. + */ + +namespace OPNsense\AcmeClient\LeValidation; + +use OPNsense\Core\Config; +use OPNsense\AcmeClient\LeAccount; +use OPNsense\AcmeClient\LeUtils; + +/** + * LeValidation stub file, contains shared logic for all validation methods. + * @package OPNsense\AcmeClient + */ +abstract class Base extends \OPNsense\AcmeClient\LeCommon +{ + public const CONFIG_PATH = 'validations.validation'; + + /** + * The validation method cannot be properly initialized without the required + * configuration. LeValidation returns a more or less uninitialized object + * that first needs to be configured, and finally initialized by this function. + * @param $certid string the ID of the certificate object + * @param $accountuuid string the UUID of the account object + * @return bool + */ + public function init(string $certid, string $accountuuid) + { + // Get config object + $this->loadConfig(self::CONFIG_PATH, $this->uuid); + + // Get account object to query ID + $account = new LeAccount($accountuuid); + if (empty($account) || $account == null) { + LeUtils::log_error("unable to load account information: ${accountuuid}"); + return false; + } + + // Store auxiliary information (required to glue stuff together) + $this->cert_id = $certid; + $this->account_id = (string)$account->getId(); + $this->account_uuid = (string)$account->getUuid(); + + // Teach acme.sh about DNS API hook location + $this->acme_env['_SCRIPT_HOME'] = '/usr/local/share/examples/acme.sh'; + + // Set log level + $this->setLoglevel(); + + // Set Let's Encrypt environment + $this->setEnvironment(); + + // Store acme hook + switch ((string)$this->config->method) { + case 'dns01': + $this->acme_args[] = '--dns ' . (string)$this->config->dns_service; + $this->acme_args[] = '--dnssleep ' . (string)$this->config->dns_sleep; + break; + case 'http01': + $this->acme_args[] = '--webroot /var/etc/acme-client/challenges'; + break; + } + + // Store acme filenames + $this->acme_args[] = '--home ' . self::ACME_HOME_DIR; + $this->acme_args[] = '--certpath ' . sprintf(self::ACME_CERT_FILE, $this->cert_id); + $this->acme_args[] = '--keypath ' . sprintf(self::ACME_KEY_FILE, $this->cert_id); + $this->acme_args[] = '--capath ' . sprintf(self::ACME_CHAIN_FILE, $this->cert_id); + $this->acme_args[] = '--fullchainpath ' . sprintf(self::ACME_FULLCHAIN_FILE, $this->cert_id); + + return true; + } + + /** + * cleanup tasks that should run after performing the certificate validation + * @return bool + */ + public function cleanup() + { + // Dummy; no default cleanup tasks. + return true; + } + + /** + * get the configured validation method (HTTP-01 or DNS-01) + * @return string validation method + */ + public function getMethod() + { + return $this->config->method; + } + + /** + * perform preparation tasks and run acme client + * @param $renew optional parameter to specify if a renewal is required + * @return bool + */ + public function run(bool $renew = false) + { + if (!($this->isEnabled())) { + LeUtils::log('ignoring disabled challenge type: ' . (string)$this->config->name); + return false; + } + + LeUtils::log('using challenge type: ' . (string)$this->config->name); + + // Issue or renew + $acme_action = $renew == true ? 'renew' : 'issue'; + + // Handle special key types + if ($this->cert_keylength == 'ec256' || $this->cert_keylength == 'ec384') { + if ($renew == true) { + // If it's a renew then pass --ecc to acme client to locate the correct cert directory + $acme_args[] = '--ecc'; + } + } + + // Use individual account config for each environment + $account_conf_dir = self::ACME_BASE_ACCOUNT_DIR . '/' . $this->account_id . '_' . $this->environment; + $account_conf_file = $account_conf_dir . '/account.conf'; + + // Preparation to run acme client + $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env['PATH'] = $this::ACME_ENV_PATH; + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + + // Run acme client + // NOTE: We "export" certificates to our own directory, so we don't have to deal + // with domain names in filesystem, but instead can use the ID of our certObj, which + // will never change. + $acmecmd = '/usr/local/sbin/acme.sh ' + . "--${acme_action} " + . implode(' ', $this->acme_args) . ' ' + . "--accountconf ${account_conf_file}"; + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); + $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + } else { + LeUtils::log_error('unable to start acme client process'); + return false; + } + + // Run optional cleanup tasks. + $this->cleanup(); + + // Check validation result + if ($result) { + LeUtils::log_error('domain validation failed (' . $this->getMethod() . ')'); + return false; + } + + return true; + } + + /** + * add config to force certificate renewal + * @param $force bool indicate whether force should be enabled or not + */ + public function setForce(bool $force = false) + { + $this->acme_args[] = $force == true ? '--force' : null; + } + + /** + * set key length + * @param $length key length + */ + public function setKey(string $length = '4096') + { + if ($length == 'ec256' || $length == 'ec384') { + $key_length = substr_replace($length, '-', 2, 0); + } else { + $key_length = $length; + } + + $this->acme_args[] = '--keylength ' . $key_length; + $this->cert_keylength = $length; + } + + /** + * configure certificate common name, altNames and DNS alias mode + */ + public function setNames(string $certname, string $altnames = '', string $aliasmode = '', string $domainalias = '', string $challengealias = '') + { + // Store basic certificate information + $this->cert_name = $certname; + $this->cert_altnames = $altnames; + $this->cert_aliasmode = $aliasmode; + $this->cert_domainalias = $domainalias; + $this->cert_challengealias = $challengealias; + + // Main domain for acme + $this->acme_args[] = '--domain ' . $certname; + + // Main domain: Use DNS alias mode for domain validation? + // https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode + if ($this->getMethod() == 'dns01') { + switch ((string)$aliasmode) { + case 'automatic': + $name = '_acme-challenge.' . ltrim((string)$this->cert_name, '*.'); + if ($dst = dns_get_record($name, DNS_CNAME)) { + $this->acme_args[] = '--domain-alias ' . $dst[0]['target']; + } + break; + case 'domain': + $this->acme_args[] = '--domain-alias ' . (string)$this->cert_domainalias; + break; + case 'challenge': + $this->acme_args[] = '--challenge-alias ' . (string)$this->cert_challengealias; + break; + } + } + + // altNames + if (!empty((string)$this->cert_altnames)) { + foreach (explode(",", (string)$this->cert_altnames) as $altname) { + $this->acme_args[] = "--domain ${altname}"; + + // altNames: Use DNS alias mode for domain validation? + // https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode + if ($this->getMethod() == 'dns01') { + switch ((string)$this->cert_aliasmode) { + case 'automatic': + $name = "_acme-challenge." . ltrim($altname, '*.'); + if ($dst = dns_get_record($name, DNS_CNAME)) { + $this->acme_args[] = '--domain-alias ' . $dst[0]['target']; + } + break; + case 'domain': + $this->acme_args[] = '--domain-alias ' . (string)$this->cert_domainalias; + break; + case 'challenge': + $this->acme_args[] = '--challenge-alias ' . (string)$this->cert_challengealias; + break; + } + } + } + } + } + + /** + * enable OCSP extension + * @param $ocsp bool whether ocsp extension should be enabled or not + */ + public function setOcsp(bool $ocsp = false) + { + // if OCSP extension is turned on pass --ocsp parameter to acme client + $this->acme_args[] = $ocsp == true ? '--ocsp' : null; + } + + /** + * set renewal interval + * @param $interval int specifies the renewal interval in days + */ + public function setRenewal(int $interval = 60) + { + $this->acme_args[] = '--days ' . (string)$interval; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Dns1984hosting.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Dns1984hosting.php new file mode 100644 index 000000000..9569797ac --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Dns1984hosting.php @@ -0,0 +1,45 @@ +acme_env['One984HOSTING_Username'] = (string)$this->config->dns_1984hosting_user; + $this->acme_env['One984HOSTING_Password'] = (string)$this->config->dns_1984hosting_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php new file mode 100644 index 000000000..4ccefe4e3 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php @@ -0,0 +1,47 @@ +acme_env['ACMEDNS_USERNAME'] = (string)$this->config->dns_acmedns_user; + $this->acme_env['ACMEDNS_PASSWORD'] = (string)$this->config->dns_acmedns_password; + $this->acme_env['ACMEDNS_SUBDOMAIN'] = (string)$this->config->dns_acmedns_subdomain; + $this->acme_env['ACMEDNS_UPDATE_URL'] = (string)$this->config->dns_acmedns_updateurl; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmeproxy.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmeproxy.php new file mode 100644 index 000000000..f2207332a --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmeproxy.php @@ -0,0 +1,46 @@ +acme_env['ACMEPROXY_ENDPOINT'] = (string)$this->config->dns_acmeproxy_endpoint; + $this->acme_env['ACMEPROXY_USERNAME'] = (string)$this->config->dns_acmeproxy_username; + $this->acme_env['ACMEPROXY_PASSWORD'] = (string)$this->config->dns_acmeproxy_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAd.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAd.php new file mode 100644 index 000000000..e468dadf6 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAd.php @@ -0,0 +1,44 @@ +acme_env['AD_API_KEY'] = (string)$this->config->dns_ad_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAli.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAli.php new file mode 100644 index 000000000..357757c6b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAli.php @@ -0,0 +1,45 @@ +acme_env['Ali_Key'] = (string)$this->config->dns_ali_key; + $this->acme_env['Ali_Secret'] = (string)$this->config->dns_ali_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArvan.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArvan.php new file mode 100644 index 000000000..786f2e395 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArvan.php @@ -0,0 +1,44 @@ +acme_env['Arvan_Token'] = (string)$this->config->dns_arvan_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAutodns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAutodns.php new file mode 100644 index 000000000..597d737a3 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAutodns.php @@ -0,0 +1,46 @@ +acme_env['AUTODNS_USER'] = (string)$this->config->dns_autodns_user; + $this->acme_env['AUTODNS_PASSWORD'] = (string)$this->config->dns_autodns_password; + $this->acme_env['AUTODNS_CONTEXT'] = (string)$this->config->dns_autodns_context; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAws.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAws.php new file mode 100644 index 000000000..c4e96479c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAws.php @@ -0,0 +1,45 @@ +acme_env['AWS_ACCESS_KEY_ID'] = (string)$this->config->dns_aws_id; + $this->acme_env['AWS_SECRET_ACCESS_KEY'] = (string)$this->config->dns_aws_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php new file mode 100644 index 000000000..1b2acc9dc --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php @@ -0,0 +1,47 @@ +acme_env['AZUREDNS_SUBSCRIPTIONID'] = (string)$this->config->dns_azuredns_subscriptionid; + $this->acme_env['AZUREDNS_TENANTID'] = (string)$this->config->dns_azuredns_tenantid; + $this->acme_env['AZUREDNS_APPID'] = (string)$this->config->dns_azuredns_appid; + $this->acme_env['AZUREDNS_CLIENTSECRET'] = (string)$this->config->dns_azuredns_clientsecret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php new file mode 100644 index 000000000..3f6f735e0 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php @@ -0,0 +1,49 @@ +acme_env['CF_Key'] = (string)$this->config->dns_cf_key; + $this->acme_env['CF_Email'] = (string)$this->config->dns_cf_email; + // Restricted API token (recommended) + $this->acme_env['CF_Token'] = (string)$this->config->dns_cf_token; + $this->acme_env['CF_Account_ID'] = (string)$this->config->dns_cf_account_id; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCloudns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCloudns.php new file mode 100644 index 000000000..fcd5b6625 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCloudns.php @@ -0,0 +1,46 @@ +acme_env['CLOUDNS_AUTH_ID'] = (string)$this->config->dns_cloudns_auth_id; + $this->acme_env['CLOUDNS_SUB_AUTH_ID'] = (string)$this->config->dns_cloudns_sub_auth_id; + $this->acme_env['CLOUDNS_AUTH_PASSWORD'] = (string)$this->config->dns_cloudns_auth_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCn.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCn.php new file mode 100644 index 000000000..93706440b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCn.php @@ -0,0 +1,45 @@ +acme_env['CN_User'] = (string)$this->config->dns_cn_user; + $this->acme_env['CN_Password'] = (string)$this->config->dns_cn_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCx.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCx.php new file mode 100644 index 000000000..c81e7b0c0 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCx.php @@ -0,0 +1,45 @@ +acme_env['CX_Key'] = (string)$this->config->dns_cx_key; + $this->acme_env['CX_Secret'] = (string)$this->config->dns_cx_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCyon.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCyon.php new file mode 100644 index 000000000..ed753b90e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCyon.php @@ -0,0 +1,45 @@ +acme_env['CY_Username'] = (string)$this->config->dns_cyon_user; + $this->acme_env['CY_Password'] = (string)$this->config->dns_cyon_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDa.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDa.php new file mode 100644 index 000000000..9f6600c42 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDa.php @@ -0,0 +1,45 @@ +acme_env['DA_Api'] = (string)$this->config->dns_da_key; + $this->acme_env['DA_Api_Insecure'] = (string)$this->config->dns_da_insecure; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDgon.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDgon.php new file mode 100644 index 000000000..d8039ccba --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDgon.php @@ -0,0 +1,44 @@ +acme_env['DO_API_KEY'] = (string)$this->config->dns_dgon_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsimple.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsimple.php new file mode 100644 index 000000000..e15c23d41 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsimple.php @@ -0,0 +1,44 @@ +acme_env['DNSimple_OAUTH_TOKEN'] = (string)$this->config->dns_dnsimple_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDo.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDo.php new file mode 100644 index 000000000..1afc8521c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDo.php @@ -0,0 +1,45 @@ +acme_env['DO_PID'] = (string)$this->config->dns_do_pid; + $this->acme_env['DO_PW'] = (string)$this->config->dns_do_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDoapi.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDoapi.php new file mode 100644 index 000000000..9dd29d2ee --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDoapi.php @@ -0,0 +1,44 @@ +acme_env['DO_LETOKEN'] = (string)$this->config->dns_doapi_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDp.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDp.php new file mode 100644 index 000000000..c118c2ecf --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDp.php @@ -0,0 +1,45 @@ +acme_env['DP_Id'] = (string)$this->config->dns_dp_id; + $this->acme_env['DP_Key'] = (string)$this->config->dns_dp_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDreamhost.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDreamhost.php new file mode 100644 index 000000000..9fd3e7750 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDreamhost.php @@ -0,0 +1,44 @@ +acme_env['DH_API_KEY'] = (string)$this->config->dns_dh_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDuckdns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDuckdns.php new file mode 100644 index 000000000..0bf4c174b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDuckdns.php @@ -0,0 +1,44 @@ +acme_env['DuckDNS_Token'] = (string)$this->config->dns_duckdns_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDyn.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDyn.php new file mode 100644 index 000000000..d11e003d4 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDyn.php @@ -0,0 +1,46 @@ +acme_env['DYN_Customer'] = (string)$this->config->dns_dyn_customer; + $this->acme_env['DYN_Username'] = (string)$this->config->dns_dyn_user; + $this->acme_env['DYN_Password'] = (string)$this->config->dns_dyn_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynu.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynu.php new file mode 100644 index 000000000..7a24ebe70 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynu.php @@ -0,0 +1,45 @@ +acme_env['Dynu_ClientId'] = (string)$this->config->dns_dynu_clientid; + $this->acme_env['Dynu_Secret'] = (string)$this->config->dns_dynu_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEuserv.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEuserv.php new file mode 100644 index 000000000..3dd2a82d9 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEuserv.php @@ -0,0 +1,46 @@ +acme_env['EUSERV_Username'] = (string)$this->config->dns_euserv_user; + $this->acme_env['EUSERV_Password'] = (string)$this->config->dns_euserv_password; + $this->acme_args[] = '--insecure'; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFreedns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFreedns.php new file mode 100644 index 000000000..d7e4d5fe2 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFreedns.php @@ -0,0 +1,45 @@ +acme_env['FREEDNS_User'] = (string)$this->config->dns_freedns_user; + $this->acme_env['FREEDNS_Password'] = (string)$this->config->dns_freedns_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php new file mode 100644 index 000000000..afebc9482 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php @@ -0,0 +1,44 @@ +acme_env['GANDI_LIVEDNS_KEY'] = (string)$this->config->dns_gandi_livedns_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php new file mode 100644 index 000000000..045363b7c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php @@ -0,0 +1,88 @@ +model->isPluginInstalled('google-cloud-sdk') != '1') { + LeUtils::log_error('Google Cloud SDK plugin is NOT installed. Please install os-google-cloud-sdk and try again.'); + return false; + } + + // A valid Google Cloud JSON key is required. + if (!empty((string)$this->config->dns_gcloud_key)) { + # Extract the gcloud project from the key data. + $_gcloud_data = json_decode((string)$this->config->dns_gcloud_key); + $gcloud_project = $_gcloud_data->project_id; + $gcloud_account = $_gcloud_data->client_email; + if (empty($gcloud_project)) { + LeUtils::log_error('unable to extract project name from Google Cloud DNS JSON key'); + return false; + } else { + LeUtils::log("Google Cloud DNS project name: ${gcloud_project}"); + } + } else { + LeUtils::log('no key for Google Cloud DNS was specified'); + return false; + } + + // Preparations to run gcloud CLI. + $val_id = (string)$this->config->id; + $gcloud_config = "acme-${val_id}"; + $gcloud_key_file = '/tmp/acme_' . (string)$this->config->dns_service . "_${val_id}.json"; + file_put_contents($gcloud_key_file, (string)$this->config->dns_gcloud_key); + chmod($gcloud_key_file, 0600); + $proc_env['CLOUDSDK_PYTHON'] = '/usr/local/bin/python3'; + $proc_env['CLOUDSDK_ACTIVE_CONFIG_NAME'] = $gcloud_config; + $proc_env['CLOUDSDK_CORE_PROJECT'] = $gcloud_project; + + // Ensure that a working gcloud config exists. + LeUtils::run_shell_command("/usr/local/bin/gcloud config configurations create ${gcloud_config}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud config configurations activate ${gcloud_config}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud auth activate-service-account --key-file=${gcloud_key_file}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud config set account ${gcloud_account}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud config set project ${gcloud_project}", $proc_env); + + // Save config for acme client. + $this->acme_env['CLOUDSDK_PYTHON'] = '/usr/local/bin/python3'; + $this->acme_env['CLOUDSDK_ACTIVE_CONFIG_NAME'] = $gcloud_config; + $this->acme_env['CLOUDSDK_CORE_PROJECT'] = $gcloud_project; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGd.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGd.php new file mode 100644 index 000000000..64a5983d7 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGd.php @@ -0,0 +1,45 @@ +acme_env['GD_Key'] = (string)$this->config->dns_gd_key; + $this->acme_env['GD_Secret'] = (string)$this->config->dns_gd_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGdnsdk.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGdnsdk.php new file mode 100644 index 000000000..034eb168c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGdnsdk.php @@ -0,0 +1,45 @@ +acme_env['GDNSDK_Username'] = (string)$this->config->dns_gdnsdk_user; + $this->acme_env['GDNSDK_Password'] = (string)$this->config->dns_gdnsdk_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHe.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHe.php new file mode 100644 index 000000000..17c17dc5b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHe.php @@ -0,0 +1,45 @@ +acme_env['HE_Username'] = (string)$this->config->dns_he_user; + $this->acme_env['HE_Password'] = (string)$this->config->dns_he_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetzner.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetzner.php new file mode 100644 index 000000000..00f755e2a --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetzner.php @@ -0,0 +1,44 @@ +acme_env['HETZNER_Token'] = (string)$this->config->dns_hetzner_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHostingde.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHostingde.php new file mode 100644 index 000000000..8b8b0f6d0 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHostingde.php @@ -0,0 +1,45 @@ +acme_env['HOSTINGDE_ENDPOINT'] = (string)$this->config->dns_hostingde_server; + $this->acme_env['HOSTINGDE_APIKEY'] = (string)$this->config->dns_hostingde_apiKey; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInfoblox.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInfoblox.php new file mode 100644 index 000000000..7cef05a44 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInfoblox.php @@ -0,0 +1,45 @@ +acme_env['Infoblox_Creds'] = (string)$this->config->dns_infoblox_credentials; + $this->acme_env['Infoblox_Server'] = (string)$this->config->dns_infoblox_server; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php new file mode 100644 index 000000000..f4e0533fb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php @@ -0,0 +1,45 @@ +acme_env['INWX_User'] = (string)$this->config->dns_inwx_user; + $this->acme_env['INWX_Password'] = (string)$this->config->dns_inws_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIspconfig.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIspconfig.php new file mode 100644 index 000000000..ea5d8df20 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIspconfig.php @@ -0,0 +1,47 @@ +acme_env['ISPC_User'] = (string)$this->config->dns_ispconfig_user; + $this->acme_env['ISPC_Password'] = (string)$this->config->dns_ispconfig_password; + $this->acme_env['ISPC_Api'] = (string)$this->config->dns_ispconfig_api; + $this->acme_env['ISPC_Api_Insecure'] = (string)$this->config->dns_ispconfig_insecure; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJoker.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJoker.php new file mode 100644 index 000000000..da74585fb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJoker.php @@ -0,0 +1,45 @@ +acme_env['JOKER_USERNAME'] = (string)$this->config->dns_joker_username; + $this->acme_env['JOKER_PASSWORD'] = (string)$this->config->dns_joker_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKinghost.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKinghost.php new file mode 100644 index 000000000..d21df40ef --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKinghost.php @@ -0,0 +1,45 @@ +acme_env['KINGHOST_username'] = (string)$this->config->dns_kinghost_username; + $this->acme_env['KINGHOST_Password'] = (string)$this->config->dns_kinghost_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKnot.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKnot.php new file mode 100644 index 000000000..a58c93ca1 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsKnot.php @@ -0,0 +1,45 @@ +acme_env['KNOT_SERVER'] = (string)$this->config->dns_knot_server; + $this->acme_env['KNOT_KEY'] = (string)$this->config->dns_knot_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLeaseweb.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLeaseweb.php new file mode 100644 index 000000000..967b3190e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLeaseweb.php @@ -0,0 +1,44 @@ +acme_env['LSW_Key'] = (string)$this->config->dns_leaseweb_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLexicon.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLexicon.php new file mode 100644 index 000000000..2ffc6d1ec --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLexicon.php @@ -0,0 +1,55 @@ +config->dns_lexicon_provider; + $env_user = 'LEXICON_' . strtoupper($provider) . '_USERNAME'; + $env_token = 'LEXICON_' . strtoupper($provider) . '_TOKEN'; + + $this->acme_env['PROVIDER'] = $provider; + $this->acme_env[$env_user] = (string)$this->config->dns_lexicon_user; + $this->acme_env[$env_token] = (string)$this->config->dns_lexicon_token; + + if ((string)$this->config->dns_lexicon_provider == 'namesilo') { + // Namesilo applies changes to DNS records only every 15 minutes. + $this->acme_args[] = '--dnssleep 960'; + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinode.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinode.php new file mode 100644 index 000000000..2b2453b8b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinode.php @@ -0,0 +1,46 @@ +acme_env['LINODE_API_KEY'] = (string)$this->config->dns_linode_key; + // Linode can take up to 15 to update DNS records + $this->acme_args[] = '--dnssleep 960'; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinodeV4.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinodeV4.php new file mode 100644 index 000000000..c69293a24 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLinodeV4.php @@ -0,0 +1,46 @@ +acme_env['LINODE_V4_API_KEY'] = (string)$this->config->dns_linode_v4_key; + // Linode can take up to 15 to update DNS records + $this->acme_args[] = '--dnssleep 960'; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLoopia.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLoopia.php new file mode 100644 index 000000000..2c5affae5 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLoopia.php @@ -0,0 +1,46 @@ +acme_env['LOOPIA_Api'] = (string)$this->config->dns_loopia_api; + $this->acme_env['LOOPIA_User'] = (string)$this->config->dns_loopia_user; + $this->acme_env['LOOPIA_Password'] = (string)$this->config->dns_loopia_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLua.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLua.php new file mode 100644 index 000000000..70d5419e5 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLua.php @@ -0,0 +1,45 @@ +acme_env['LUA_Key'] = (string)$this->config->dns_lua_key; + $this->acme_env['LUA_Email'] = (string)$this->config->dns_lua_email; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMe.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMe.php new file mode 100644 index 000000000..c177f24a2 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMe.php @@ -0,0 +1,45 @@ +acme_env['ME_Key'] = (string)$this->config->dns_me_key; + $this->acme_env['ME_Secret'] = (string)$this->config->dns_me_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMiab.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMiab.php new file mode 100644 index 000000000..f75b188fb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMiab.php @@ -0,0 +1,46 @@ +acme_env['MIAB_Username'] = (string)$this->config->dns_miab_user; + $this->acme_env['MIAB_Password'] = (string)$this->config->dns_miab_password; + $this->acme_env['MIAB_Server'] = (string)$this->config->dns_miab_server; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecheap.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecheap.php new file mode 100644 index 000000000..5d252d3e9 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecheap.php @@ -0,0 +1,51 @@ +acme_env['NAMECHEAP_USERNAME'] = (string)$this->config->dns_namecheap_user; + $this->acme_env['NAMECHEAP_API_KEY'] = (string)$this->config->dns_namecheap_api; + if (!empty((string)$this->config->dns_namecheap_sourceip)) { + $this->acme_env['NAMECHEAP_SOURCEIP'] = (string)$this->config->dns_namecheap_sourceip; + } else { + // Use a public service to get our source IP for Namecheap API + $this->acme_env['NAMECHEAP_SOURCEIP'] = 'https://ifconfig.co/ip'; + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecom.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecom.php new file mode 100644 index 000000000..f011500b7 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamecom.php @@ -0,0 +1,45 @@ +acme_env['Namecom_Username'] = (string)$this->config->dns_namecom_user; + $this->acme_env['Namecom_Token'] = (string)$this->config->dns_namecom_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamesilo.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamesilo.php new file mode 100644 index 000000000..1cfa7f326 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNamesilo.php @@ -0,0 +1,46 @@ +acme_env['Namesilo_Key'] = (string)$this->config->dns_namesilo_key; + // Namesilo applies changes to DNS records only every 15 minutes. + $this->acme_args[] = '--dnssleep 960'; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNetcup.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNetcup.php new file mode 100644 index 000000000..b4afca6cb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNetcup.php @@ -0,0 +1,48 @@ +acme_env['NC_CID'] = (string)$this->config->dns_netcup_cid; + $this->acme_env['NC_Apikey'] = (string)$this->config->dns_netcup_key; + $this->acme_env['NC_Apipw'] = (string)$this->config->dns_netcup_pw; + // netcup applies changes to DNS records only every 10 minutes. + $this->acme_args[] = '--dnssleep 600'; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsone.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsone.php new file mode 100644 index 000000000..c1df9f7ad --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsone.php @@ -0,0 +1,44 @@ +acme_env['NS1_Key'] = (string)$this->config->dns_nsone_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php new file mode 100644 index 000000000..5fe117e5e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php @@ -0,0 +1,52 @@ +cert_uuid); + $secret_key_filename = "${configdir}/secret.key"; + $secret_key_data = (string)$this->config->dns_nsupdate_key . '\n'; + file_put_contents($secret_key_filename, $secret_key_data); + + // Add env variables + $this->acme_env['NSUPDATE_KEY'] = $secret_key_filename; + $this->acme_env['NSUPDATE_SERVER'] = (string)$this->config->dns_nsupdate_server; + $this->acme_env['NSUPDATE_ZONE'] = (string)$this->config->dns_nsupdate_zone; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOpnsense.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOpnsense.php new file mode 100644 index 000000000..fcb33a702 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOpnsense.php @@ -0,0 +1,54 @@ +model->isPluginInstalled('bind') != '1') { + LeUtils::log_error('BIND plugin is NOT installed. Please install os-bind and try again.'); + return false; + } + $this->acme_env['OPNs_Host'] = (string)$this->config->dns_opnsense_host; + $this->acme_env['OPNs_Port'] = (string)$this->config->dns_opnsense_port; + $this->acme_env['OPNs_Key'] = (string)$this->config->dns_opnsense_key; + $this->acme_env['OPNs_Token'] = (string)$this->config->dns_opnsense_token; + $this->acme_env['OPNs_Api_Insecure'] = (string)$this->config->dns_opnsense_insecure; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOvh.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOvh.php new file mode 100644 index 000000000..8d9896924 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOvh.php @@ -0,0 +1,47 @@ +acme_env['OVH_AK'] = (string)$this->config->dns_ovh_app_key; + $this->acme_env['OVH_AS'] = (string)$this->config->dns_ovh_app_secret; + $this->acme_env['OVH_CK'] = (string)$this->config->dns_ovh_consumer_key; + $this->acme_env['OVH_END_POINT'] = (string)$this->config->dns_ovh_endpoint; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPdns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPdns.php new file mode 100644 index 000000000..7029fed79 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPdns.php @@ -0,0 +1,46 @@ +acme_env['PDNS_Url'] = (string)$this->config->dns_pdns_url; + $this->acme_env['PDNS_ServerId'] = (string)$this->config->dns_pdns_serverid; + $this->acme_env['PDNS_Token'] = (string)$this->config->dns_pdns_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPleskxml.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPleskxml.php new file mode 100644 index 000000000..5b71dcd53 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPleskxml.php @@ -0,0 +1,46 @@ +acme_env['pleskxml_user'] = (string)$this->config->dns_pleskxml_user; + $this->acme_env['pleskxml_pass'] = (string)$this->config->dns_pleskxml_pass; + $this->acme_env['pleskxml_uri'] = (string)$this->config->dns_pleskxml_uri; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSchlundtech.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSchlundtech.php new file mode 100644 index 000000000..c287c1d60 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSchlundtech.php @@ -0,0 +1,45 @@ +acme_env['SCHLUNDTECH_USER'] = (string)$this->config->dns_schlundtech_user; + $this->acme_env['SCHLUNDTECH_PASSWORD'] = (string)$this->config->dns_schlundtech_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php new file mode 100644 index 000000000..56e311266 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php @@ -0,0 +1,44 @@ +acme_env['SL_Key'] = (string)$this->config->dns_sl_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsServercow.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsServercow.php new file mode 100644 index 000000000..1ab91b2d2 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsServercow.php @@ -0,0 +1,45 @@ +acme_env['SERVERCOW_API_Username'] = (string)$this->config->dns_servercow_username; + $this->acme_env['SERVERCOW_API_Password'] = (string)$this->config->dns_servercow_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUnoeuro.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUnoeuro.php new file mode 100644 index 000000000..79dd3a054 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUnoeuro.php @@ -0,0 +1,45 @@ +acme_env['UNO_Key'] = (string)$this->config->dns_uno_key; + $this->acme_env['UNO_User'] = (string)$this->config->dns_uno_user; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVariomedia.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVariomedia.php new file mode 100644 index 000000000..e519d4a85 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVariomedia.php @@ -0,0 +1,44 @@ +acme_env['VARIOMEDIA_API_TOKEN'] = (string)$this->config->dns_variomedia_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVscale.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVscale.php new file mode 100644 index 000000000..566611aae --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsVscale.php @@ -0,0 +1,44 @@ +acme_env['VSCALE_API_KEY'] = (string)$this->config->dns_vscale_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsYandex.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsYandex.php new file mode 100644 index 000000000..73891677e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsYandex.php @@ -0,0 +1,44 @@ +acme_env['PDD_Token'] = (string)$this->config->dns_yandex_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZilore.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZilore.php new file mode 100644 index 000000000..854724c65 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZilore.php @@ -0,0 +1,44 @@ +acme_env['Zilore_Key'] = (string)$this->config->dns_zilore_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZonomi.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZonomi.php new file mode 100644 index 000000000..ffe93baaa --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZonomi.php @@ -0,0 +1,44 @@ +acme_env['ZM_Key'] = (string)$this->config->dns_zm_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php new file mode 100644 index 000000000..32f6d3138 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php @@ -0,0 +1,140 @@ +object(); + $local_http_port = $configObj->OPNsense->AcmeClient->settings->challengePort; + + // Collect all IP addresses here, automatic port forward will be applied for each IP + $iplist = array(); + + // Add IP addresses from auto-discovery feature + if ($this->config->http_opn_autodiscovery == 1) { + $dnslist = explode(',', $this->cert_altnames); + $dnslist[] = $this->cert_name; + foreach ($dnslist as $fqdn) { + // NOTE: This may take some time. + $ip_found = gethostbyname("${fqdn}."); + if (!empty($ip_found)) { + $iplist[] = (string)$ip_found; + } + } + } + + // Add IP addresses from user input + $additional_ip = (string)$this->config->http_opn_ipaddresses; + if (!empty($additional_ip)) { + foreach (explode(',', $additional_ip) as $ip) { + $iplist[] = $ip; + } + } + + // Add IP address from chosen interface + if (!empty((string)$this->config->http_opn_interface)) { + $interface_ip = get_interface_ip((string)$this->config->http_opn_interface); + if (!empty($interface_ip)) { + $iplist[] = $interface_ip; + } + } + + // Check if IPv6 support is enabled + if (isset($configObj->system->ipv6allow) && ($configObj->system->ipv6allow == '1')) { + $_ipv6_enabled = true; + } else { + $_ipv6_enabled = false; + } + + // Generate rules for all IP addresses + $anchor_rules = ""; + if (!empty($iplist)) { + $dedup_iplist = array_unique($iplist); + // Add one rule for every IP + foreach ($dedup_iplist as $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + // IPv4 + $_dst = '127.0.0.1'; + $_family = 'inet'; + LeUtils::log("using IPv4 address: ${ip}"); + } elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) { + // IPv6 + $_dst = '::1'; + $_family = 'inet6'; + LeUtils::log("using IPv6 address: ${ip}"); + } else { + continue; // skip broken entries + } + $anchor_rules .= "rdr pass ${_family} proto tcp from any to ${ip} port 80 -> ${_dst} port ${local_http_port}\n"; + } + } else { + LeUtils::log_error("no IP addresses found to setup port forward"); + return false; + } + + // Abort if no rules were generated + if (empty($anchor_rules)) { + LeUtils::log_error("unable to setup a port forward (empty ruleset)"); + return false; + } + + // Create temporary port forward to allow acme challenges to get through + $anchor_setup = "rdr-anchor \"acme-client\"\n"; + file_put_contents("${configdir}/acme_anchor_setup", $anchor_setup); + chmod("${configdir}/acme_anchor_setup", 0600); + mwexec("/sbin/pfctl -f ${configdir}/acme_anchor_setup"); + file_put_contents("${configdir}/acme_anchor_rules", $anchor_rules); + chmod("${configdir}/acme_anchor_rules", 0600); + mwexec("/sbin/pfctl -a acme-client -f ${configdir}/acme_anchor_rules"); + } + + public function cleanup() + { + // Flush OPNsense port forward rules. + mwexec('/sbin/pfctl -a acme-client -F all'); + + // Workaround to solve disconnection issues reported by some users. + $backend = new \OPNsense\Core\Backend(); + $response = $backend->configdRun('filter reload'); + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php new file mode 100644 index 000000000..f8113483e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php @@ -0,0 +1,90 @@ +getNodeByReference(self::CONFIG_PATH . '.' . $uuid); + if ($obj == null) { + LeUtils::log_error("challenge type not found: ${uuid}"); + return null; + } + + // Get type of validation to find the required class name. + switch ((string)$obj->method) { + case 'dns01': + $search_name = $obj->dns_service; + break; + case 'http01': + $search_name = "http_" . $obj->http_service; + break; + } + + // Convert to PascalCase + $val_name = str_replace(' ', '', ucwords(str_replace(array('-', '_'), ' ', $search_name))); + + // Search class name + foreach (glob(__DIR__ . "/LeValidation/*.php") as $filename) { + $srv_found = basename($filename, '.php'); + try { + $reflClass = new \ReflectionClass("OPNsense\\AcmeClient\\LeValidation\\{$srv_found}"); + } catch (\ReflectionException $e) { + break; + } + if ($reflClass->implementsInterface('OPNsense\\AcmeClient\\LeValidationInterface')) { + if ($srv_found == $val_name) { + // Create new object + $objVal = $reflClass->newInstance(); + $objVal->setUuid($uuid); + return $objVal; + } + } + } + LeUtils::log_error("challenge type not supported: " . (string)$objVal->method . " (${uuid})"); + return null; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationInterface.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationInterface.php new file mode 100644 index 000000000..f5836672e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationInterface.php @@ -0,0 +1,49 @@ + //OPNsense/AcmeClient - 1.6.2 + 2.0.0 A secure Let's Encrypt plugin @@ -143,10 +143,21 @@ N - + N + + + N + 100 + 100 + 1000 + + + + N + @@ -272,8 +283,7 @@ N - - + 100 100 1000 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 472b15b89..79a8f7cf8 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 @@ -6,9 +6,9 @@ - + - + diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M2_0_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M2_0_0.php new file mode 100644 index 000000000..3bd34fe87 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M2_0_0.php @@ -0,0 +1,54 @@ +getNodeByReference('accounts.account')->iterateItems() as $account) { + if (!empty((string)$account->lastUpdate) && empty((string)$account->statusLastUpdate)) { + $account->statusLastUpdate = (string)$account->lastUpdate; + // Account is already registered. + $account->statusCode = '200'; + $account->lastUpdate = null; // clear old value + } elseif (!empty((string)$account->statusLastUpdate) || !empty((string)$account->statusCode)) { + // Ignore accounts that already use M2_0_0 fields. + } else { + // Account registration is pending. + $account->statusCode = '100'; + } + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt index f6d6e8ed3..7a95eef4b 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt @@ -35,18 +35,284 @@ POSSIBILITY OF SUCH DAMAGE. * link grid actions **********************************************************************/ - $("#grid-accounts").UIBootgrid( - { search:'/api/acmeclient/accounts/search', - get:'/api/acmeclient/accounts/get/', - set:'/api/acmeclient/accounts/update/', - add:'/api/acmeclient/accounts/add/', - del:'/api/acmeclient/accounts/del/', - toggle:'/api/acmeclient/accounts/toggle/', - options: { - rowCount:[10,25,50,100,500,1000] + var gridParams = { + search:'/api/acmeclient/accounts/search', + get:'/api/acmeclient/accounts/get/', + set:'/api/acmeclient/accounts/update/', + add:'/api/acmeclient/accounts/add/', + del:'/api/acmeclient/accounts/del/', + toggle:'/api/acmeclient/accounts/toggle/', + register:'/api/acmeclient/accounts/register/', + }; + + var gridopt = { + ajax: true, + selection: true, + multiSelect: true, + rowCount:[10,25,50,100,500,1000], + url: '/api/acmeclient/accounts/search', + formatters: { + "commands": function (column, row) { + return " " + + "" + + "" + + ""; + }, + "rowtoggle": function (column, row) { + if (parseInt(row[column.id], 2) == 1) { + return ""; + } else { + return ""; + } + }, + "accountstatus": function (column, row) { + if (row.statusCode == "" || row.statusCode == undefined) { + // fallback to lastUpdate value (unset if account was not registered) + if (row.statusLastUpdate == "" || row.statusLastUpdate == undefined) { + return "{{ lang._('not registered') }}"; + } else { + return "{{ lang._('OK') }}"; + } + } else if (row.statusCode == "100") { + return "{{ lang._('not registered') }}"; + } else if (row.statusCode == "200") { + return "{{ lang._('OK (registered)') }}"; + } else if (row.statusCode == "250") { + return "{{ lang._('deactivated') }}"; + } else if (row.statusCode == "300") { + return "{{ lang._('configuration error') }}"; + } else if (row.statusCode == "400") { + return "{{ lang._('registration failed') }}"; + } else if (row.statusCode == "500") { + return "{{ lang._('internal error') }}"; + } else { + return "{{ lang._('unknown') }}"; + } + }, + "acmestatusdate": function (column, row) { + if (row.statusLastUpdate == "" || row.statusCode == undefined) { + return "{{ lang._('unknown') }}"; + } else { + var statusdate = new Date(row.statusLastUpdate*1000); + return statusdate.toLocaleString(); + } } - } - ); + }, + }; + + /** + * reload bootgrid, return to current selected page + */ + function std_bootgrid_reload(gridId) { + var currentpage = $("#"+gridId).bootgrid("getCurrentPage"); + $("#"+gridId).bootgrid("reload"); + // absolutely not perfect, bootgrid.reload doesn't seem to support when().done() + setTimeout(function(){ + $('#'+gridId+'-footer a[data-page="'+currentpage+'"]').click(); + }, 400); + } + + /** + * copy actions for selected items from opnsense_bootgrid_plugin.js + */ + var grid_accounts = $("#grid-accounts").bootgrid(gridopt).on("loaded.rs.jquery.bootgrid", function (e) + { + // scale footer on resize + $(this).find("tfoot td:first-child").attr('colspan',$(this).find("th").length - 1); + $(this).find('tr[data-row-id]').each(function(){ + if ($(this).find('[class*="command-toggle"]').first().data("value") == "0") { + $(this).addClass("text-muted"); + } + }); + + // edit dialog id to use + var editDlg = $(this).attr('data-editDialog'); + var gridId = $(this).attr('id'); + + // link Add new to child button with data-action = add + $(this).find("*[data-action=add]").click(function(){ + if ( gridParams['get'] != undefined && gridParams['add'] != undefined) { + var urlMap = {}; + urlMap['frm_' + editDlg] = gridParams['get']; + mapDataToFormUI(urlMap).done(function(){ + // update selectors + formatTokenizersUI(); + $('.selectpicker').selectpicker('refresh'); + // clear validation errors (if any) + clearFormValidation('frm_' + editDlg); + }); + + // show dialog for edit + $('#'+editDlg).modal({backdrop: 'static', keyboard: false}); + // + $("#btn_"+editDlg+"_save").unbind('click').click(function(){ + saveFormToEndpoint(url=gridParams['add'], + formid='frm_' + editDlg, callback_ok=function(){ + $("#"+editDlg).modal('hide'); + $("#"+gridId).bootgrid("reload"); + }, true); + }); + } else { + console.log("[grid] action add missing") + } + }); + + // link delete selected items action + $(this).find("*[data-action=deleteSelected]").click(function(){ + if ( gridParams['del'] != undefined) { + stdDialogConfirm('{{ lang._('Confirm removal') }}', + '{{ lang._('Do you want to remove the selected item?') }}', + '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function () { + var rows =$("#"+gridId).bootgrid('getSelectedRows'); + if (rows != undefined){ + var deferreds = []; + $.each(rows, function(key,uuid){ + deferreds.push(ajaxCall(url=gridParams['del'] + uuid, sendData={},null)); + }); + // refresh after load + $.when.apply(null, deferreds).done(function(){ + std_bootgrid_reload(gridId); + }); + } + }); + } else { + console.log("[grid] action del missing") + } + }); + + }); + + /** + * copy actions for items from opnsense_bootgrid_plugin.js + */ + grid_accounts.on("loaded.rs.jquery.bootgrid", function(){ + + // edit dialog id to use + var editDlg = $(this).attr('data-editDialog'); + var gridId = $(this).attr('id'); + + // edit item + grid_accounts.find(".command-edit").on("click", function(e) + { + if (editDlg != undefined && gridParams['get'] != undefined) { + var uuid = $(this).data("row-id"); + var urlMap = {}; + urlMap['frm_' + editDlg] = gridParams['get'] + uuid; + mapDataToFormUI(urlMap).done(function () { + // update selectors + formatTokenizersUI(); + $('.selectpicker').selectpicker('refresh'); + // clear validation errors (if any) + clearFormValidation('frm_' + editDlg); + }); + + // show dialog for pipe edit + $('#'+editDlg).modal({backdrop: 'static', keyboard: false}); + // define save action + $("#btn_"+editDlg+"_save").unbind('click').click(function(){ + if (gridParams['set'] != undefined) { + saveFormToEndpoint(url=gridParams['set']+uuid, + formid='frm_' + editDlg, callback_ok=function(){ + $("#"+editDlg).modal('hide'); + std_bootgrid_reload(gridId); + }, true); + } else { + console.log("[grid] action set missing") + } + }); + } else { + console.log("[grid] action get or data-editDialog missing") + } + }); + + // copy item, save as new + grid_accounts.find(".command-copy").on("click", function(e) + { + if (editDlg != undefined && gridParams['get'] != undefined) { + var uuid = $(this).data("row-id"); + var urlMap = {}; + urlMap['frm_' + editDlg] = gridParams['get'] + uuid; + mapDataToFormUI(urlMap).done(function () { + // update selectors + formatTokenizersUI(); + $('.selectpicker').selectpicker('refresh'); + // clear validation errors (if any) + clearFormValidation('frm_' + editDlg); + }); + + // show dialog for pipe edit + $('#'+editDlg).modal({backdrop: 'static', keyboard: false}); + // define save action + $("#btn_"+editDlg+"_save").unbind('click').click(function(){ + if (gridParams['add'] != undefined) { + saveFormToEndpoint(url=gridParams['add'], + formid='frm_' + editDlg, callback_ok=function(){ + $("#"+editDlg).modal('hide'); + std_bootgrid_reload(gridId); + }, true); + } else { + console.log("[grid] action add missing") + } + }); + } else { + console.log("[grid] action get or data-editDialog missing") + } + }); + + // delete item + grid_accounts.find(".command-delete").on("click", function(e) + { + if (gridParams['del'] != undefined) { + var uuid=$(this).data("row-id"); + stdDialogConfirm('{{ lang._('Confirm removal') }}', + '{{ lang._('Do you want to remove the selected item?') }}', + '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function () { + ajaxCall(url=gridParams['del'] + uuid, + sendData={},callback=function(data,status){ + // reload grid after delete + $("#"+gridId).bootgrid("reload"); + }); + }); + } else { + console.log("[grid] action del missing") + } + }); + + // toggle item + grid_accounts.find(".command-toggle").on("click", function(e) + { + if (gridParams['toggle'] != undefined) { + var uuid=$(this).data("row-id"); + $(this).addClass("fa-spinner fa-pulse"); + ajaxCall(url=gridParams['toggle'] + uuid, + sendData={},callback=function(data,status){ + // reload grid after toggle + std_bootgrid_reload(gridId); + }); + } else { + console.log("[grid] action toggle missing") + } + }); + + // register account + grid_accounts.find(".command-register").on("click", function(e) + { + if (gridParams['register'] != undefined) { + var uuid=$(this).data("row-id"); + stdDialogConfirm('{{ lang._('Confirmation Required') }}', + '{{ lang._('Register the selected account with Lets Encrypt?') }}', + '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { + ajaxCall(url=gridParams['register'] + uuid,sendData={},callback=function(data,status){ + // reload grid afterwards + $("#"+gridId).bootgrid("reload"); + }); + }); + } else { + console.log("[grid] action register missing") + } + }); + + }); }); @@ -64,6 +330,8 @@ POSSIBILITY OF SUCH DAMAGE. {{ lang._('Enabled') }} {{ lang._('Name') }} {{ lang._('E-Mail') }} + {{ lang._('Status') }} + {{ lang._('Registration Date') }} {{ lang._('Commands') }} {{ lang._('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 6db878d50..2d172eb01 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 @@ -318,13 +318,12 @@ POSSIBILITY OF SUCH DAMAGE. }); // sign cert - // TODO: this should block other acme.sh actions grid_certificates.find(".command-sign").on("click", function(e) { if (gridParams['sign'] != undefined) { var uuid=$(this).data("row-id"); stdDialogConfirm('{{ lang._('Confirmation Required') }}', - '{{ lang._('Forcefully (re-)issue the selected certificate?') }}', + '{{ lang._('Forcefully issue or renew the selected certificate?') }}', '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { // Handle HAProxy integration (no-op if not applicable) ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { @@ -340,7 +339,6 @@ POSSIBILITY OF SUCH DAMAGE. }); // revoke cert - // TODO: this should block other acme.sh actions grid_certificates.find(".command-revoke").on("click", function(e) { if (gridParams['revoke'] != undefined) { @@ -360,7 +358,6 @@ 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) { @@ -380,7 +377,6 @@ POSSIBILITY OF SUCH DAMAGE. }); // run automation - // TODO: this should block other acme.sh actions grid_certificates.find(".command-automation").on("click", function(e) { if (gridParams['automation'] != undefined) { @@ -417,7 +413,6 @@ POSSIBILITY OF SUCH DAMAGE. /** * Sign or renew ALL certificates - * 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/mvc/app/views/OPNsense/AcmeClient/settings.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt index 08d8932b8..bb67c87c0 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt @@ -241,7 +241,7 @@ POSSIBILITY OF SUCH DAMAGE.
- {{ lang._("Please read the official %sLet's Encrypt documentation%s before using this plugin. Otherwise you will easily hit its %srate limits%s and thus all your attempts to issue a certificate will fail.") | format('', '', '', '') }}{{ lang._("Please use Let's Encrypt's %sstaging servers%s when using this plugin for the first time or while testing a new validation method. You will have to reissue your certificates when switching from staging to production servers to get valid certificates.") | format('', '') }} + {{ lang._("Please read the official %sLet's Encrypt documentation%s before using this plugin. Otherwise you will easily hit its %srate limits%s and thus all your attempts to issue a certificate will fail.") | format('', '', '', '') }}{{ lang._("Please use Let's Encrypt's %sstaging servers%s when using this plugin for the first time or while testing a new challenge type. You will have to reissue your certificates when switching from staging to production servers to get valid certificates.") | format('', '') }}
{{ lang._('Please use the %sissue tracker%s to report bugs or request new features.') | format('', '') }}
diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt index cb94acec0..6ece1b0aa 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -91,7 +91,7 @@ POSSIBILITY OF SUCH DAMAGE.
@@ -122,4 +122,4 @@ POSSIBILITY OF SUCH DAMAGE.
{# include dialogs #} -{{ partial("layout_partials/base_dialog",['fields':formDialogValidation,'id':'DialogValidation','label':lang._('Edit Validation Method')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogValidation,'id':'DialogValidation','label':lang._('Edit Challenge Type')])}} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php deleted file mode 100755 index 928961e39..000000000 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php +++ /dev/null @@ -1,1655 +0,0 @@ -#!/usr/local/bin/php - - * Copyright (C) 2008 Shrew Soft Inc. - * 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. - */ - -// Hello. I am the spaghetti monster. Yummy. - -// Use legacy code to manage certificates. -require_once("config.inc"); -require_once("certs.inc"); -require_once("legacy_bindings.inc"); -require_once("interfaces.inc"); -require_once("util.inc"); - -// Some stuff requires the almighty MVC framework. -use OPNsense\Core\Backend; -use OPNsense\Core\Config; -use OPNsense\Base; -use OPNsense\AcmeClient\AcmeClient; - -$postponed_updates = array(); - -/* CLI arguments: - * -a (action) - * -c (certificate id, NOT the uuid) - * -A (all certificates) - * -C (cron, special rules apply when running as cronjob) - * -F (force, rewew/recreate) - * -S (staging) - */ -$options = getopt("a:c:ACFS"); - -// Simple validation -if (!isset($options["a"]) or (!isset($options["c"]) and !isset($options["A"]))) { - // ALL actions require either a certificate ID or the -A switch - echo "ERROR: not enough arguments\n"; - exit(1); -} -if (($options["a"] == 'revoke') and !isset($options["c"])) { - echo "ERROR: option revoke requires a certificate ID\n"; - exit(1); -} - -// Cron mode -if (isset($options["C"])) { - // Automatically work on ALL certificates - $options["A"] = ""; -} - -// Run the specified action -switch ($options["a"]) { - case 'sign': - $result = cert_action_validator($options["c"]); - echo json_encode(array('status' => $result)); - break; - case 'renew': - $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 = cert_action_validator($options["c"]); - echo json_encode(array('status' => $result)); - break; - case 'automation': - $result = cert_action_validator($options["c"]); - echo json_encode(array('status' => $result)); - break; - default: - echo "ERROR: invalid argument specified\n"; - log_error("invalid argument specified"); - exit(1); -} - -// Write certificate status updates to configuration -dump_postponed_updates(); - -// ALL certificate work starts here. First we do some common validation and -// make sure that everything is prepared for acme client to run. -// The actual issue/renew/revoke work is done by separate functions. -function cert_action_validator($opt_cert_id) -{ - global $options; - - $modelObj = new OPNsense\AcmeClient\AcmeClient(); - - // Store certs here after successful issue/renewal. Required for automations. - $restart_certs = array(); - - // Search for cert ID in configuration - $configObj = Config::getInstance()->object(); - if (isset($configObj->OPNsense->AcmeClient->certificates) && $configObj->OPNsense->AcmeClient->certificates->count() > 0) { - foreach ($configObj->OPNsense->AcmeClient->certificates->children() as $certObj) { - // Extract cert ID - $cert_id = (string)$certObj->id; - if (empty($cert_id)) { - continue; // Cert is invalid, skip it. - } - - // Either work with ALL certificates or check if cert ID matches - 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 - } - // 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 - $acctRef = (string)$certObj->account; - $acctObj = null; - $acctref_found = false; - foreach ($modelObj->getNodeByReference('accounts.account')->iterateItems() as $node) { - if ((string)$node->getAttributes()["uuid"] == $acctRef) { - $acctref_found = true; - $acctObj = $node; - break; // Match! Go ahead. - } - } - - // Make sure we found the configured account - if ($acctref_found == true) { - // Ensure that this account was properly setup and registered. - $acct_result = run_acme_account_registration($acctObj, $certObj, $modelObj); - if (!$acct_result) { - // account registration OK - } else { - log_error("AcmeClient: account registration failed"); - log_cert_acme_status($certObj, $modelObj, '400'); - if (isset($options["A"])) { - continue; // skip to next item - } - return(1); - } - } else { - log_error("AcmeClient: account not found"); - log_cert_acme_status($certObj, $modelObj, '300'); - if (isset($options["A"])) { - continue; // skip to next item - } - return(1); - } - - // Extract Validation Method from referenced obj - $valRef = (string)$certObj->validationMethod; - $valObj = null; - $ref_found = false; - foreach ($modelObj->getNodeByReference('validations.validation')->iterateItems() as $node) { - if ((string)$node->getAttributes()["uuid"] == $valRef) { - $ref_found = true; - $valObj = $node; - break; // Match! Go ahead. - } - } - - // 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); - } - } - - // Only run certificate automation - if ($options["a"] == "automation") { - // Check if the cert was successul issued - if (!empty((string)$certObj->statusCode) and (string)$certObj->statusCode == '200') { - log_error("AcmeClient: ready to run automation for certificate: " . (string)$certObj->name); - $restart_certs[] = $certObj; - } else { - log_error("AcmeClient: failed to run automation, certificate status not OK: " . (string)$certObj->name); - return(1); - } - break; // Stop after first match. - } - - // Make sure we found the configured validation method - if ($ref_found == true) { - // Was a revocation requested? - // NOTE: Revocation is not even considered when some elements have already been - // deleted from the GUI. It's likely that it would fail anyway. - if ($options["a"] == "revoke") { - // 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 - } - return(1); - } - } - - // Which validation method? - if ((string)$valObj->method == 'http01' or ((string)$valObj->method == 'dns01')) { - // Start acme client to issue or renew certificate - $val_result = run_acme_validation($certObj, $valObj, $acctObj); - if (!$val_result) { - log_error("AcmeClient: successfully issued/renewed certificate: " . (string)$certObj->name); - // Import certificate to Cert Manager - if (!import_certificate($certObj, $modelObj)) { - // Prepare certificate for automation - $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 - } - return(1); - } - } elseif ($val_result == '99') { - // Renewal not required. Do nothing. - } 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 - } - return(1); - } - } 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 - } - return(1); - } - } 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 - } - return(1); - } - - // Work on ALL certificates? - if (!isset($options["A"])) { - break; // Stop after first match. - } - } - } - } else { - log_error("AcmeClient: no LE certificates found in configuration"); - return(1); - } - - // Run automations if an operation was successful. - if (!empty($restart_certs)) { - // Execute automations. - if (!run_restart_actions($restart_certs, $modelObj)) { - # Success. - } else { - log_error("AcmeClient: failed to execute some automations"); - } - } - - return(0); -} - -// Prepare optional parameters for acme client -function eval_optional_acme_args() -{ - global $options; - $configObj = Config::getInstance()->object(); - - $acme_args = array(); - - // Force certificate renewal? - $acme_args[] = isset($options["F"]) ? "--force" : null; - - // Use LE staging environment? - $acme_args[] = $configObj->OPNsense->AcmeClient->settings->environment == "stg" ? "--staging" : null; - $acme_args[] = isset($options["S"]) ? "--staging" : null; // for debug purpose - - // Set log level - switch ($configObj->OPNsense->AcmeClient->settings->logLevel) { - case "extended": - $acme_args[] = "--log-level 2"; - case "debug": - $acme_args[] = "--debug"; - case "debug2": - $acme_args[] = "--debug 2"; - case "debug3": - $acme_args[] = "--debug 3"; - default: - $acme_args[] = "--log-level 1"; - } - - // Remove empty and duplicate elements from array - return(array_unique(array_filter($acme_args))); -} - -// Create account keys and register accounts, export/import them from/to filesystem/config.xml -function run_acme_account_registration($acctObj, $certObj, $modelObj) -{ - global $options; - - // Prepare optional parameters for acme-client - $acme_args = eval_optional_acme_args(); - - // Collect account information - $acme_env = (string)$modelObj->settings->environment; - $account_conf_dir = "/var/etc/acme-client/accounts/" . $acctObj->id . "_${acme_env}"; - $account_conf_file = $account_conf_dir . "/account.conf"; - $account_key_file = $account_conf_dir . "/account.key"; - $account_json_file = $account_conf_dir . "/account.json"; - $account_ca_file = $account_conf_dir . "/ca.conf"; - $acme_conf = array(); - $acme_conf[] = "CERT_HOME='/var/etc/acme-client/home'"; - $acme_conf[] = "LOG_FILE='/var/log/acme.sh.log'"; - $acme_conf[] = "ACCOUNT_KEY_PATH='" . $account_key_file . "'"; - $acme_conf[] = "ACCOUNT_JSON_PATH='" . $account_json_file . "'"; - $acme_conf[] = "CA_CONF='" . $account_ca_file . "'"; - if (!empty((string)$acctObj->email)) { - $acme_conf[] = "ACCOUNT_EMAIL='" . (string)$acctObj->email . "'"; - } - - // Create account configuration file - if (!is_dir($account_conf_dir)) { - mkdir($account_conf_dir, 0700, true); - } - file_put_contents($account_conf_file, (string)implode("\n", $acme_conf) . "\n"); - chmod($account_conf_file, 0600); - - // Check if account key already exists - if (is_file($account_key_file)) { - // account key found - } else { - // Check if we have an account key in our configuration - if (!empty((string)$acctObj->key)) { - // Write key to disk - file_put_contents($account_key_file, (string)base64_decode((string)$acctObj->key)); - chmod($account_key_file, 0600); - } else { - // Do not generate new key if a revocation was requested. - if ($options["a"] == "revoke") { - log_error("AcmeClient: account key not found, but a revocation was requested"); - return(1); - } - - // Let acme client generate a new account key - $acmecmd = "/usr/local/sbin/acme.sh " - . implode(" ", $acme_args) . " " - . "--createAccountKey " - . "--accountkeylength 4096 " - . "--home /var/etc/acme-client/home " - . "--accountconf " . $account_conf_file; - $result = mwexec($acmecmd); - - // Check exit code - if (!($result)) { - // created a new account key - } else { - log_error("AcmeClient: failed to create a new account key"); - return(1); - } - - // Read account key - $account_key_content = @file_get_contents($account_key_file); - if ($account_key_content == false) { - log_error("AcmeClient: unable to read account key from file"); - return(1); - } - - // Import account key into config - $acctObj->key = base64_encode($account_key_content); - // serialize to config and save - $modelObj->serializeToConfig(); - Config::getInstance()->save(); - Config::getInstance()->forceReload(); - } - } - - // Check if account was already registered - if (!empty((string)$acctObj->lastUpdate)) { - // account key already registered - } else { - // Do not register new account if a revocation was requested. - if ($options["a"] == "revoke") { - log_error("AcmeClient: account not registered, but a revocation was requested"); - return(1); - } - - // Run acme client to register the account - $acmecmd = "/usr/local/sbin/acme.sh " - . implode(" ", $acme_args) . " " - . "--registeraccount " - . "--home /var/etc/acme-client/home " - . "--accountconf " . $account_conf_file; - $result = mwexec($acmecmd); - - // Check exit code - if (!($result)) { - // registered a new account key - } else { - log_error("AcmeClient: failed to register a new account key"); - return(1); - } - - // Set update/create time in config - $acctObj->lastUpdate = time(); - // serialize to config and save - $modelObj->serializeToConfig(); - Config::getInstance()->save(); - Config::getInstance()->forceReload(); - } - - return; -} - -// Run acme client with HTTP-01 or DNS-01 validation to issue/renew certificate -function run_acme_validation($certObj, $valObj, $acctObj) -{ - global $options; - - // Required to run pre-defined commands. - $backend = new Backend(); - $modelObj = new OPNsense\AcmeClient\AcmeClient(); - - // Collect account information - $acme_env = (string)$modelObj->settings->environment; - $account_conf_dir = "/var/etc/acme-client/accounts/" . $acctObj->id . "_${acme_env}"; - $account_conf_file = $account_conf_dir . "/account.conf"; - - // Generate certificate filenames - $cert_id = (string)$certObj->id; - $cert_filename = "/var/etc/acme-client/certs/${cert_id}/cert.pem"; - $cert_chain_filename = "/var/etc/acme-client/certs/${cert_id}/chain.pem"; - $cert_fullchain_filename = "/var/etc/acme-client/certs/${cert_id}/fullchain.pem"; - $key_filename = "/var/etc/acme-client/keys/${cert_id}/private.key"; - - // Setup our own ACME environment - $certdir = "/var/etc/acme-client/certs/${cert_id}"; - $keydir = "/var/etc/acme-client/keys/${cert_id}"; - $configdir = "/var/etc/acme-client/configs/${cert_id}"; - foreach (array($certdir, $keydir, $configdir) as $dir) { - if (!is_dir($dir)) { - mkdir($dir, 0700, true); - } - } - - // Preparation to run acme client - $acme_args = eval_optional_acme_args(); - $proc_env = array(); // env variables for proc_open() - $proc_env['PATH'] = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin'; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Do we need to issue or renew the certificate? - if (!empty((string)$certObj->lastUpdate) and !isset($options["F"])) { - $acme_action = "renew"; - } else { - // Default: Issue a new certificate. - // If "-F" is specified, forcefully re-issue the cert, no matter if it's required. - // NOTE: This is useful if altNames were changed or when switching - // from acme staging to acme production servers. - $acme_action = "issue"; - } - - // Calculate next renewal date - $last_update = !empty((string)$certObj->lastUpdate) ? (string)$certObj->lastUpdate : 0; - $renew_cert = false; - $current_time = new \DateTime(); - $last_update_time = new \DateTime(); - $last_update_time->setTimestamp($last_update); - $renew_interval = (string)$certObj->renewInterval; - $next_update = $last_update_time->add(new \DateInterval('P' . $renew_interval . 'D')); - - // Check if it's time to renew the cert. - if (isset($options["F"]) or ($current_time >= $next_update)) { - $renew_cert = true; - } else { - // Renewal not yet required, report special code - return(99); - } - - // Try HTTP-01 or DNS-01 validation? - $val_method = (string)$valObj->method; - $acme_validation = ""; // val.method as argument for acme.sh - $acme_hook_options = array(); // store addition arguments for acme.sh here - switch ($val_method) { - case 'http01': - $acme_validation = "--webroot /var/etc/acme-client/challenges "; - break; - case 'dns01': - $acme_validation = "--dns " . (string)$valObj->dns_service . " "; - break; - default: - log_error("AcmeClient: invalid validation method specified: " . (string)$valObj->method); - return(1); - } - - // HTTP-01: setup OPNsense internal port forward - if (($val_method == 'http01') and ((string)$valObj->http_service == 'opnsense')) { - // Get configured HTTP port for local lighttpd server - $configObj = Config::getInstance()->object(); - $local_http_port = $configObj->OPNsense->AcmeClient->settings->challengePort; - - // Collect all IP addresses here, automatic port forward will be applied for each IP - $iplist = array(); - - // Add IP addresses from auto-discovery feature - if ($valObj->http_opn_autodiscovery == 1) { - $dnslist = explode(',', $certObj->altNames); - $dnslist[] = $certObj->name; - foreach ($dnslist as $fqdn) { - // NOTE: This may take some time. - $ip_found = gethostbyname("${fqdn}."); - if (!empty($ip_found)) { - $iplist[] = (string)$ip_found; - } - } - } - - // Add IP addresses from user input - $additional_ip = (string)$valObj->http_opn_ipaddresses; - if (!empty($additional_ip)) { - foreach (explode(',', $additional_ip) as $ip) { - $iplist[] = $ip; - } - } - - // Add IP address from chosen interface - if (!empty((string)$valObj->http_opn_interface)) { - $interface_ip = get_interface_ip((string)$valObj->http_opn_interface); - if (!empty($interface_ip)) { - $iplist[] = $interface_ip; - } - } - - // Check wether IPv6 support is enabled - $configObj = Config::getInstance()->object(); - if (isset($configObj->system->ipv6allow) && ($configObj->system->ipv6allow == "1")) { - $_ipv6_enabled = true; - } else { - $_ipv6_enabled = false; - } - - // Generate rules for all IP addresses - $anchor_rules = ""; - if (!empty($iplist)) { - $dedup_iplist = array_unique($iplist); - // Add one rule for every IP - foreach ($dedup_iplist as $ip) { - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - // IPv4 - $_dst = '127.0.0.1'; - $_family = 'inet'; - log_error("AcmeClient: using IPv4 address: ${ip}"); - } elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) { - // IPv6 - $_dst = '::1'; - $_family = 'inet6'; - log_error("AcmeClient: using IPv6 address: ${ip}"); - } else { - continue; // skip broken entries - } - $anchor_rules .= "rdr pass ${_family} proto tcp from any to ${ip} port 80 -> ${_dst} port ${local_http_port}\n"; - } - } else { - log_error("AcmeClient: no IP addresses found to setup port forward"); - return(1); - } - - // Abort if no rules were generated - if (empty($anchor_rules)) { - log_error("AcmeClient: unable to setup a port forward (empty ruleset)"); - return(1); - } - - // Create temporary port forward to allow acme challenges to get through - $anchor_setup = "rdr-anchor \"acme-client\"\n"; - file_put_contents("${configdir}/acme_anchor_setup", $anchor_setup); - chmod("${configdir}/acme_anchor_setup", 0600); - mwexec("/sbin/pfctl -f ${configdir}/acme_anchor_setup"); - file_put_contents("${configdir}/acme_anchor_rules", $anchor_rules); - chmod("${configdir}/acme_anchor_rules", 0600); - mwexec("/sbin/pfctl -a acme-client -f ${configdir}/acme_anchor_rules"); - } - - // Prepare DNS-01 hooks - if ($val_method == 'dns01') { - // Some common stuff - $val_id = preg_replace("/[^a-zA-Z0-9]/", "", (string)$valObj->id); - $secret_key_filename = "${configdir}/secret.key"; - $acme_args[] = '--dnssleep ' . $valObj->dns_sleep; - - // Setup DNS hook: - // Set required env variables, write secrets to files, etc. - switch ((string)$valObj->dns_service) { - case 'dns_1984hosting': - $proc_env['One984HOSTING_Username'] = (string)$valObj->dns_1984hosting_user; - $proc_env['One984HOSTING_Password'] = (string)$valObj->dns_1984hosting_password; - break; - case 'dns_acmedns': - $proc_env['ACMEDNS_USERNAME'] = (string)$valObj->dns_acmedns_user; - $proc_env['ACMEDNS_PASSWORD'] = (string)$valObj->dns_acmedns_password; - $proc_env['ACMEDNS_SUBDOMAIN'] = (string)$valObj->dns_acmedns_subdomain; - $proc_env['ACMEDNS_UPDATE_URL'] = (string)$valObj->dns_acmedns_updateurl; - break; - case 'dns_acmeproxy': - $proc_env['ACMEPROXY_ENDPOINT'] = (string)$valObj->dns_acmeproxy_endpoint; - $proc_env['ACMEPROXY_USERNAME'] = (string)$valObj->dns_acmeproxy_username; - $proc_env['ACMEPROXY_PASSWORD'] = (string)$valObj->dns_acmeproxy_password; - break; - case 'dns_ad': - $proc_env['AD_API_KEY'] = (string)$valObj->dns_ad_key; - break; - case 'dns_ali': - $proc_env['Ali_Key'] = (string)$valObj->dns_ali_key; - $proc_env['Ali_Secret'] = (string)$valObj->dns_ali_secret; - break; - case 'dns_arvan': - $proc_env['Arvan_Token'] = (string)$valObj->dns_arvan_token; - break; - case 'dns_autodns': - $proc_env['AUTODNS_USER'] = (string)$valObj->dns_autodns_user; - $proc_env['AUTODNS_PASSWORD'] = (string)$valObj->dns_autodns_password; - $proc_env['AUTODNS_CONTEXT'] = (string)$valObj->dns_autodns_context; - break; - case 'dns_aws': - $proc_env['AWS_ACCESS_KEY_ID'] = (string)$valObj->dns_aws_id; - $proc_env['AWS_SECRET_ACCESS_KEY'] = (string)$valObj->dns_aws_secret; - break; - case 'dns_azure': - $proc_env['AZUREDNS_SUBSCRIPTIONID'] = (string)$valObj->dns_azuredns_subscriptionid; - $proc_env['AZUREDNS_TENANTID'] = (string)$valObj->dns_azuredns_tenantid; - $proc_env['AZUREDNS_APPID'] = (string)$valObj->dns_azuredns_appid; - $proc_env['AZUREDNS_CLIENTSECRET'] = (string)$valObj->dns_azuredns_clientsecret; - break; - case 'dns_cf': - // Global API key (insecure) - $proc_env['CF_Key'] = (string)$valObj->dns_cf_key; - $proc_env['CF_Email'] = (string)$valObj->dns_cf_email; - // Restricted API token (recommended) - $proc_env['CF_Token'] = (string)$valObj->dns_cf_token; - $proc_env['CF_Account_ID'] = (string)$valObj->dns_cf_account_id; - break; - case 'dns_cloudns': - $proc_env['CLOUDNS_AUTH_ID'] = (string)$valObj->dns_cloudns_auth_id; - $proc_env['CLOUDNS_SUB_AUTH_ID'] = (string)$valObj->dns_cloudns_sub_auth_id; - $proc_env['CLOUDNS_AUTH_PASSWORD'] = (string)$valObj->dns_cloudns_auth_password; - break; - case 'dns_cn': - $proc_env['CN_User'] = (string)$valObj->dns_cn_user; - $proc_env['CN_Password'] = (string)$valObj->dns_cn_password; - break; - case 'dns_cx': - $proc_env['CX_Key'] = (string)$valObj->dns_cx_key; - $proc_env['CX_Secret'] = (string)$valObj->dns_cx_secret; - break; - case 'dns_cyon': - $proc_env['CY_Username'] = (string)$valObj->dns_cyon_user; - $proc_env['CY_Password'] = (string)$valObj->dns_cyon_user; - break; - case 'dns_da': - $proc_env['DA_Api'] = (string)$valObj->dns_da_key; - $proc_env['DA_Api_Insecure'] = (string)$valObj->dns_da_insecure; - break; - case 'dns_dgon': - $proc_env['DO_API_KEY'] = (string)$valObj->dns_dgon_key; - break; - case 'dns_dnsimple': - $proc_env['DNSimple_OAUTH_TOKEN'] = (string)$valObj->dns_dnsimple_token; - break; - case 'dns_do': - $proc_env['DO_PID'] = (string)$valObj->dns_do_pid; - $proc_env['DO_PW'] = (string)$valObj->dns_do_password; - break; - case 'dns_doapi': - $proc_env['DO_LETOKEN'] = (string)$valObj->dns_doapi_token; - break; - case 'dns_dp': - $proc_env['DP_Id'] = (string)$valObj->dns_dp_id; - $proc_env['DP_Key'] = (string)$valObj->dns_dp_key; - break; - case 'dns_dreamhost': - $proc_env['DH_API_KEY'] = (string)$valObj->dns_dh_key; - break; - case 'dns_duckdns': - $proc_env['DuckDNS_Token'] = (string)$valObj->dns_duckdns_token; - break; - case 'dns_dyn': - $proc_env['DYN_Customer'] = (string)$valObj->dns_dyn_customer; - $proc_env['DYN_Username'] = (string)$valObj->dns_dyn_user; - $proc_env['DYN_Password'] = (string)$valObj->dns_dyn_password; - break; - case 'dns_dynu': - $proc_env['Dynu_ClientId'] = (string)$valObj->dns_dynu_clientid; - $proc_env['Dynu_Secret'] = (string)$valObj->dns_dynu_secret; - break; - case 'dns_euserv': - $proc_env['EUSERV_Username'] = (string)$valObj->dns_euserv_user; - $proc_env['EUSERV_Password'] = (string)$valObj->dns_euserv_password; - $acme_hook_options[] = "--insecure"; - break; - case 'dns_freedns': - $proc_env['FREEDNS_User'] = (string)$valObj->dns_freedns_user; - $proc_env['FREEDNS_Password'] = (string)$valObj->dns_freedns_password; - break; - case 'dns_gandi_livedns': - $proc_env['GANDI_LIVEDNS_KEY'] = (string)$valObj->dns_gandi_livedns_key; - break; - case 'dns_gcloud': - # Google Cloud SDK must be installed. - if ((string)$modelObj->isPluginInstalled('google-cloud-sdk') != "1") { - log_error("AcmeClient: Google Cloud SDK plugin is NOT installed. Please install os-google-cloud-sdk."); - return(1); - } - # We need a valid Google Cloud JSON key. - if (!empty((string)$valObj->dns_gcloud_key)) { - # Extract the gcloud project from the key data. - $_gcloud_data = json_decode((string)$valObj->dns_gcloud_key); - $gcloud_project = $_gcloud_data->project_id; - $gcloud_account = $_gcloud_data->client_email; - if (empty($gcloud_project)) { - log_error("AcmeClient: unable to extract project name from Google Cloud DNS JSON key"); - return(1); - } else { - log_error("AcmeClient: Google Cloud DNS project name: ${gcloud_project}"); - } - } else { - log_error("AcmeClient: no key for Google Cloud DNS was specified"); - return(1); - } - # Preparations for gcloud CLI. - $gcloud_config = "acme-${val_id}"; - $gcloud_key_file = "/tmp/acme_" . (string)$valObj->dns_service . "_${val_id}.json"; - file_put_contents($gcloud_key_file, (string)$valObj->dns_gcloud_key); - chmod($gcloud_key_file, 0600); - $proc_env['CLOUDSDK_ACTIVE_CONFIG_NAME'] = $gcloud_config; - $proc_env['CLOUDSDK_CORE_PROJECT'] = $gcloud_project; - # Ensure that a working gcloud config exists. - run_shell_command("/usr/local/bin/gcloud config configurations create ${gcloud_config}", $proc_env); - run_shell_command("/usr/local/bin/gcloud config configurations activate ${gcloud_config}", $proc_env); - run_shell_command("/usr/local/bin/gcloud auth activate-service-account --key-file=${gcloud_key_file}", $proc_env); - run_shell_command("/usr/local/bin/gcloud config set account ${gcloud_account}", $proc_env); - run_shell_command("/usr/local/bin/gcloud config set project ${gcloud_project}", $proc_env); - break; - case 'dns_gd': - $proc_env['GD_Key'] = (string)$valObj->dns_gd_key; - $proc_env['GD_Secret'] = (string)$valObj->dns_gd_secret; - break; - case 'dns_gdnsdk': - $proc_env['GDNSDK_Username'] = (string)$valObj->dns_gdnsdk_user; - $proc_env['GDNSDK_Password'] = (string)$valObj->dns_gdnsdk_password; - break; - case 'dns_hetzner': - $proc_env['HETZNER_Token'] = (string)$valObj->dns_hetzner_token; - break; - case 'dns_hostingde': - $proc_env['HOSTINGDE_ENDPOINT'] = (string)$valObj->dns_hostingde_server; - $proc_env['HOSTINGDE_APIKEY'] = (string)$valObj->dns_hostingde_apiKey; - break; - case 'dns_he': - $proc_env['HE_Username'] = (string)$valObj->dns_he_user; - $proc_env['HE_Password'] = (string)$valObj->dns_he_password; - break; - case 'dns_infoblox': - $proc_env['Infoblox_Creds'] = (string)$valObj->dns_infoblox_credentials; - $proc_env['Infoblox_Server'] = (string)$valObj->dns_infoblox_server; - break; - case 'dns_inwx': - $proc_env['INWX_User'] = (string)$valObj->dns_inwx_user; - $proc_env['INWX_Password'] = (string)$valObj->dns_inws_password; - break; - case 'dns_ispconfig': - $proc_env['ISPC_User'] = (string)$valObj->dns_ispconfig_user; - $proc_env['ISPC_Password'] = (string)$valObj->dns_ispconfig_password; - $proc_env['ISPC_Api'] = (string)$valObj->dns_ispconfig_api; - $proc_env['ISPC_Api_Insecure'] = (string)$valObj->dns_ispconfig_insecure; - break; - case 'dns_joker': - $proc_env['JOKER_USERNAME'] = (string)$valObj->dns_joker_username; - $proc_env['JOKER_PASSWORD'] = (string)$valObj->dns_joker_password; - break; - case 'dns_kinghost': - $proc_env['KINGHOST_username'] = (string)$valObj->dns_kinghost_username; - $proc_env['KINGHOST_Password'] = (string)$valObj->dns_kinghost_password; - break; - case 'dns_knot': - $proc_env['KNOT_SERVER'] = (string)$valObj->dns_knot_server; - $proc_env['KNOT_KEY'] = (string)$valObj->dns_knot_key; - break; - case 'dns_leaseweb': - $proc_env['LSW_Key'] = (string)$valObj->dns_leaseweb_key; - break; - case 'dns_lexicon': - $proc_env['PROVIDER'] = (string)$valObj->dns_lexicon_provider; - $proc_env['LEXICON_' . strtoupper($proc_env['PROVIDER']) . '_USERNAME'] = (string)$valObj->dns_lexicon_user; - $proc_env['LEXICON_' . strtoupper($proc_env['PROVIDER']) . '_TOKEN'] = (string)$valObj->dns_lexicon_token; - if ((string)$valObj->dns_lexicon_provider == 'namesilo') { - // Namesilo applies changes to DNS records only every 15 minutes. - $acme_hook_options[] = "--dnssleep 960"; - } - break; - case 'dns_linode': - $proc_env['LINODE_API_KEY'] = (string)$valObj->dns_linode_key; - // Linode can take up to 15 to update DNS records - $acme_hook_options[] = "--dnssleep 960"; - break; - case 'dns_linode_v4': - $proc_env['LINODE_V4_API_KEY'] = (string)$valObj->dns_linode_v4_key; - // Linode can take up to 15 to update DNS records - $acme_hook_options[] = "--dnssleep 960"; - break; - case 'dns_loopia': - $proc_env['LOOPIA_Api'] = (string)$valObj->dns_loopia_api; - $proc_env['LOOPIA_User'] = (string)$valObj->dns_loopia_user; - $proc_env['LOOPIA_Password'] = (string)$valObj->dns_loopia_password; - break; - case 'dns_lua': - $proc_env['LUA_Key'] = (string)$valObj->dns_lua_key; - $proc_env['LUA_Email'] = (string)$valObj->dns_lua_email; - break; - case 'dns_me': - $proc_env['ME_Key'] = (string)$valObj->dns_me_key; - $proc_env['ME_Secret'] = (string)$valObj->dns_me_secret; - break; - case 'dns_miab': - $proc_env['MIAB_Username'] = (string)$valObj->dns_miab_user; - $proc_env['MIAB_Password'] = (string)$valObj->dns_miab_password; - $proc_env['MIAB_Server'] = (string)$valObj->dns_miab_server; - break; - case 'dns_namecheap': - $proc_env['NAMECHEAP_USERNAME'] = (string)$valObj->dns_namecheap_user; - $proc_env['NAMECHEAP_API_KEY'] = (string)$valObj->dns_namecheap_api; - if (!empty((string)$valObj->dns_namecheap_sourceip)) { - $proc_env['NAMECHEAP_SOURCEIP'] = (string)$valObj->dns_namecheap_sourceip; - } else { - // Use a public service to get our source IP for Namecheap API - $proc_env['NAMECHEAP_SOURCEIP'] = 'https://ifconfig.co/ip'; - } - break; - case 'dns_namecom': - $proc_env['Namecom_Username'] = (string)$valObj->dns_namecom_user; - $proc_env['Namecom_Token'] = (string)$valObj->dns_namecom_token; - break; - case 'dns_namesilo': - $proc_env['Namesilo_Key'] = (string)$valObj->dns_namesilo_key; - // Namesilo applies changes to DNS records only every 15 minutes. - $acme_hook_options[] = "--dnssleep 960"; - break; - case 'dns_netcup': - $proc_env['NC_CID'] = (string)$valObj->dns_netcup_cid; - $proc_env['NC_Apikey'] = (string)$valObj->dns_netcup_key; - $proc_env['NC_Apipw'] = (string)$valObj->dns_netcup_pw; - // netcup applies changes to DNS records only every 10 minutes. - $acme_hook_options[] = "--dnssleep 600"; - break; - case 'dns_nsone': - $proc_env['NS1_Key'] = (string)$valObj->dns_nsone_key; - break; - case 'dns_nsupdate': - // Write secret key to filesystem - $secret_key_data = (string)$valObj->dns_nsupdate_key . "\n"; - file_put_contents($secret_key_filename, $secret_key_data); - $proc_env['NSUPDATE_KEY'] = $secret_key_filename; - $proc_env['NSUPDATE_SERVER'] = (string)$valObj->dns_nsupdate_server; - $proc_env['NSUPDATE_ZONE'] = (string)$valObj->dns_nsupdate_zone; - break; - case 'dns_opnsense': - # BIND plugin must be installed. - if ((string)$modelObj->isPluginInstalled('bind') != "1") { - log_error("AcmeClient: BIND plugin is NOT installed. Please install os-bind."); - return(1); - } - $proc_env['OPNs_Host'] = (string)$valObj->dns_opnsense_host; - $proc_env['OPNs_Port'] = (string)$valObj->dns_opnsense_port; - $proc_env['OPNs_Key'] = (string)$valObj->dns_opnsense_key; - $proc_env['OPNs_Token'] = (string)$valObj->dns_opnsense_token; - $proc_env['OPNs_Api_Insecure'] = (string)$valObj->dns_opnsense_insecure; - break; - case 'dns_ovh': - $proc_env['OVH_AK'] = (string)$valObj->dns_ovh_app_key; - $proc_env['OVH_AS'] = (string)$valObj->dns_ovh_app_secret; - $proc_env['OVH_CK'] = (string)$valObj->dns_ovh_consumer_key; - $proc_env['OVH_END_POINT'] = (string)$valObj->dns_ovh_endpoint; - break; - case 'dns_pdns': - $proc_env['PDNS_Url'] = (string)$valObj->dns_pdns_url; - $proc_env['PDNS_ServerId'] = (string)$valObj->dns_pdns_serverid; - $proc_env['PDNS_Token'] = (string)$valObj->dns_pdns_token; - break; - case 'dns_pleskxml': - $proc_env['pleskxml_user'] = (string)$valObj->dns_pleskxml_user; - $proc_env['pleskxml_pass'] = (string)$valObj->dns_pleskxml_pass; - $proc_env['pleskxml_uri'] = (string)$valObj->dns_pleskxml_uri; - break; - case 'dns_schlundtech': - $proc_env['SCHLUNDTECH_USER'] = (string)$valObj->dns_schlundtech_user; - $proc_env['SCHLUNDTECH_PASSWORD'] = (string)$valObj->dns_schlundtech_password; - break; - case 'dns_selectel': - $proc_env['SL_Key'] = (string)$valObj->dns_sl_key; - break; - case 'dns_servercow': - $proc_env['SERVERCOW_API_Username'] = (string)$valObj->dns_servercow_username; - $proc_env['SERVERCOW_API_Password'] = (string)$valObj->dns_servercow_password; - break; - case 'dns_unoeuro': - $proc_env['UNO_Key'] = (string)$valObj->dns_uno_key; - $proc_env['UNO_User'] = (string)$valObj->dns_uno_user; - break; - case 'dns_variomedia': - $proc_env['VARIOMEDIA_API_TOKEN'] = (string)$valObj->dns_variomedia_key; - break; - case 'dns_vscale': - $proc_env['VSCALE_API_KEY'] = (string)$valObj->dns_vscale_key; - break; - case 'dns_yandex': - $proc_env['PDD_Token'] = (string)$valObj->dns_yandex_token; - break; - case 'dns_zilore': - $proc_env['Zilore_Key'] = (string)$valObj->dns_zilore_key; - break; - case 'dns_zonomi': - $proc_env['ZM_Key'] = (string)$valObj->dns_zm_key; - break; - default: - log_error("AcmeClient: invalid DNS-01 service specified: " . (string)$valObj->dns_service); - return(1); - } - } - - // Prepare altNames - $altnames = ""; - - // Main domain: Use DNS alias mode for domain validation? - // https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode - if ($val_method == 'dns01') { - switch ((string)$certObj->aliasmode) { - case 'automatic': - $name = "_acme-challenge." . ltrim((string)$certObj->name, '*.'); - if ($dst = dns_get_record($name, DNS_CNAME)) { - $altnames .= "--domain-alias " . $dst[0]['target'] . " "; - } - break; - case 'domain': - $altnames .= "--domain-alias " . (string)$certObj->domainalias . " "; - break; - case 'challenge': - $altnames .= "--challenge-alias " . (string)$certObj->challengealias . " "; - break; - } - } - - if (!empty((string)$certObj->altNames)) { - $_altnames = explode(",", (string)$certObj->altNames); - foreach (explode(",", (string)$certObj->altNames) as $altname) { - $altnames .= "--domain ${altname} "; - - // altNames: Use DNS alias mode for domain validation? - // https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode - if ($val_method == 'dns01') { - switch ((string)$certObj->aliasmode) { - case 'automatic': - $name = "_acme-challenge." . ltrim($altname, '*.'); - if ($dst = dns_get_record($name, DNS_CNAME)) { - $altnames .= "--domain-alias " . $dst[0]['target'] . " "; - } - break; - case 'domain': - $altnames .= "--domain-alias " . (string)$certObj->domainalias . " "; - break; - case 'challenge': - $altnames .= "--challenge-alias " . (string)$certObj->challengealias . " "; - break; - } - } - } - } - - // Teach acme.sh about DNS API hook location - $proc_env['_SCRIPT_HOME'] = '/usr/local/share/examples/acme.sh'; - - // Get the chosen key length from xml and trim the parameter before passing to acme client - $key_length = (string) $certObj->keyLength; - $key_length = substr($key_length, 4); - - if ($key_length == 'ec256' || $key_length == 'ec384') { - if ($acme_action == "renew") { - // if it's renew then pass --ecc to acme client to locate the correct cert directory - $acme_args[] = "--ecc"; - } - $key_length = substr_replace($key_length, '-', 2, 0); - } - - // if OCSP Extension is turned on pass --ocsp parameter to acme client - if (isset($certObj->ocsp) and ($certObj->ocsp == 1)) { - $acme_args[] = "--ocsp"; - } - - // Run acme client - // NOTE: We "export" certificates to our own directory, so we don't have to deal - // with domain names in filesystem, but instead can use the ID of our certObj. - $acmecmd = "/usr/local/sbin/acme.sh " - . implode(" ", $acme_args) . " " - . "--${acme_action} " - . "--days " . (string)$certObj->renewInterval . " " - . "--domain " . (string)$certObj->name . " " - . $altnames - . $acme_validation . " " - . "--home /var/etc/acme-client/home " - . "--keylength " . $key_length . " " - . "--accountconf " . $account_conf_file . " " - . "--certpath ${cert_filename} " - . "--keypath ${key_filename} " - . "--capath ${cert_chain_filename} " - . "--fullchainpath ${cert_fullchain_filename} " - . implode(" ", $acme_hook_options); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - log_error("AcmeClient: unable to start acme client process"); - return(1); - } - - // HTTP-01: flush OPNsense port forward rules - if (($val_method == 'http01') and ((string)$valObj->http_service == 'opnsense')) { - mwexec('/sbin/pfctl -a acme-client -F all'); - // XXX: workaround to solve disconnection issues reported by some users - $response = $backend->configdRun('filter reload'); - } - - // Check validation result - if ($result) { - log_error("AcmeClient: domain validation failed"); - return(1); - } - - // Simply return acme clients exit code - return($result); -} - -// Revoke a certificate. -function revoke_cert($certObj, $valObj, $acctObj) -{ - // NOTE: Revocation will fail if additional domain names were added - // to the certificate after issue/renewal. - - // Prepare optional parameters for acme-client - $acme_args = eval_optional_acme_args(); - - // Collect account information - $acme_env = (string)$modelObj->settings->environment; - $account_conf_dir = "/var/etc/acme-client/accounts/" . $acctObj->id . "_${acme_env}"; - $account_conf_file = $account_conf_dir . "/account.conf"; - - // 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 - // NOTE: We "export" certificates to our own directory, so we don't have to deal - // with domain names in filesystem, but instead can use the ID of our certObj. - $acmecmd = "/usr/local/sbin/acme.sh " - . implode(" ", $acme_args) . " " - . "--revoke " - . "--domain " . (string)$certObj->name . " " - . "--home /var/etc/acme-client/home " - . "--accountconf " . $account_conf_file . " " - . $ecc_param; - $result = mwexec($acmecmd); - - // 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); -} - -function import_certificate($certObj, $modelObj) -{ - global $config; - - $cert_id = (string)$certObj->id; - $cert_filename = "/var/etc/acme-client/certs/${cert_id}/cert.pem"; - $cert_chain_filename = "/var/etc/acme-client/certs/${cert_id}/chain.pem"; - $cert_fullchain_filename = "/var/etc/acme-client/certs/${cert_id}/fullchain.pem"; - $key_filename = "/var/etc/acme-client/keys/${cert_id}/private.key"; - - // Check if certificate files can be found - clearstatcache(); // don't let the cache fool us - foreach (array($cert_filename, $key_filename, $cert_chain_filename, $cert_fullchain_filename) as $file) { - if (is_file($file)) { - // certificate file found - } else { - log_error("AcmeClient: unable to import certificate, file not found: ${file}"); - return(1); - } - } - - /* - * Step 1: import CA - */ - - // Read contents from CA file - $ca_content = @file_get_contents($cert_chain_filename); - if ($ca_content != false) { - $ca_subject = cert_get_subject($ca_content, false); - $ca_serial = cert_get_serial($ca_content, false); - $ca_cn = local_cert_get_cn($ca_content, false); - $ca_issuer = cert_get_issuer($ca_content, false); - $ca_purpose = cert_get_purpose($ca_content, false); - } else { - log_error("AcmeClient: unable to read CA certificate content from file"); - return(1); - } - - // Prepare CA for import in Cert Manager - $ca = array(); - $ca['crt'] = base64_encode($ca_content); - $ca['refid'] = uniqid(); - $ca_found = false; - - // Check if CA was previously imported - $cacnt = 0; - foreach ($config['ca'] as $cacrt) { - $cacrt_subject = cert_get_subject($cacrt['crt'], true); - $cacrt_issuer = cert_get_issuer($cacrt['crt'], true); - if (($ca_subject == $cacrt_subject) and ($ca_issuer == $cacrt_issuer)) { - // Use old refid instead of generating a new one - $ca['refid'] = (string)$cacrt['refid']; - $ca_found = true; - break; - } - $cacnt++; - } - - // Collect required CA information - $ca_cn = local_cert_get_cn($ca_content, false); - $ca['descr'] = (string)$ca_cn . ' (Let\'s Encrypt)'; - - // Prepare CA for import - local_ca_import($ca, $ca_content); - - // Update existing CA? - if ($ca_found == true) { - $config['ca'][$cacnt] = $ca; - } else { - // Create new CA item - $config['ca'][] = $ca; - log_error("AcmeClient: importing Let's Encrypt CA: ${ca_cn}"); - } - - /* - * Step 2: import certificate - */ - - // Read contents from certificate file - $cert_content = @file_get_contents($cert_filename); - if ($cert_content != false) { - $cert_subject = cert_get_subject($cert_content, false); - $cert_serial = cert_get_serial($cert_content, false); - $cert_cn = local_cert_get_cn($cert_content, false); - $cert_issuer = cert_get_issuer($cert_content, false); - $cert_purpose = cert_get_purpose($cert_content, false); - } else { - log_error("AcmeClient: unable to read certificate content from file"); - return(1); - } - - // Prepare certificate for import in Cert Manager - $cert = array(); - $cert_refid = uniqid(); - $cert['refid'] = $cert_refid; - $cert['caref'] = (string)$ca['refid']; - $import_log_message = 'Imported'; - $cert_found = false; - - // Check if cert was previously imported - if (isset($certObj->certRefId)) { - // Check if the imported certificate can still be found - $configObj = Config::getInstance()->object(); - foreach ($configObj->cert as $cfgCert) { - // Check if the IDs matches - if ((string)$certObj->certRefId == (string)$cfgCert->refid) { - $cert_found = true; - break; - } - } - // Existing cert? - if ($cert_found == true) { - // Use old refid instead of generating a new one - $cert_refid = (string)$certObj->certRefId; - $import_log_message = 'Updated'; - } - } else { - // Not found. Just import as new cert. - } - - // Read private key - $key_content = @file_get_contents($key_filename); - if ($key_content == false) { - log_error("AcmeClient: unable to read private key from file: ${key_filename}"); - return(1); - } - - // Collect required cert information - $cert_cn = local_cert_get_cn($cert_content, false); - $cert['descr'] = (string)$cert_cn . ' (Let\'s Encrypt)'; - $cert['refid'] = $cert_refid; - - // Prepare certificate for import - cert_import($cert, $cert_content, $key_content); - - // Update existing certificate? - if ($cert_found == true) { - // FIXME: Do legacy configs really depend on counters? - $cnt = 0; - foreach ($config['cert'] as $crt) { - if ($crt['refid'] == $cert_refid) { - $config['cert'][$cnt] = $cert; - break; - } - $cnt++; - } - } else { - // Create new certificate item - $config['cert'][] = $cert; - } - - /* - * Step 3: update configuration - */ - - // Write changes to config - // TODO: Legacy code, should be replaced with code from OPNsense framework - write_config("${import_log_message} Let's Encrypt X.509 certificate: ${cert_cn}"); - log_error("AcmeClient: ${import_log_message} Let's Encrypt X.509 certificate: ${cert_cn}"); - - // Update (acme) certificate object (through MVC framework) - $uuid = $certObj->attributes()->uuid; - $node = $modelObj->getNodeByReference('certificates.certificate.' . $uuid); - if ($node != null) { - // Add refid to certObj - $node->certRefId = $cert_refid; - // Set update/create time - $node->lastUpdate = time(); - // if node was found, serialize to config and save - $modelObj->serializeToConfig(); - Config::getInstance()->save(); - Config::getInstance()->forceReload(); - } else { - log_error("AcmeClient: unable to update LE certificate object"); - return(1); - } - - return(0); -} - -function run_restart_actions($certlist, $modelObj) -{ - global $config; - $return = 0; - $configObj = Config::getInstance()->object(); - - // Required to run pre-defined commands. - $backend = new Backend(); - - // NOTE: Do NOT run any automation twice, collect duplicates first. - $restart_actions = array(); - - // Check if there's something to do. - if (!empty($certlist) and is_array($certlist)) { - // Extract cert object - foreach ($certlist as $certObj) { - // Make sure the object is functional. - if (empty($certObj->id)) { - log_error("AcmeClient: failed to query certificate for automation"); - continue; - } - // Extract automations - if (empty((string)$certObj->restartActions)) { - // No automations configured. - continue; - } - $_actions = explode(',', $certObj->restartActions); - // Walk through all linked automations. - foreach ($_actions as $_action) { - // Extract automations - $action = $modelObj->getByActionID($_action); - // Make sure the object is functional. - if ($action === null) { - log_error("AcmeClient: failed to retrieve automations from certificate"); - } else { - // Ignore disabled automations (even if they are still - // linked to a certificated). - if ((string)$action->enabled === "0") { - continue; - } - // Store by UUID, automatically eliminates duplicates. - $_data = array(); - $_data['obj'] = $action; - $_data['cert_id'] = $certObj->id; - $restart_actions[$_action] = $_data; - } - } - } - } - - // Run the collected automations. - if (!empty($restart_actions) and is_array($restart_actions)) { - // Extract cert object - 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 automation: " . $action->name); - switch ((string)$action->type) { - case 'restart_gui': - $response = $backend->configdRun('webgui restart 2', true); - break; - case 'restart_haproxy': - $response = $backend->configdRun("haproxy restart"); - break; - case 'restart_nginx': - $response = $backend->configdRun("nginx restart"); - break; - case 'upload_highwinds': - $response = $backend->configdRun("acmeclient upload_highwinds ${cert_id} ${action_id}"); - break; - case 'upload_sftp': - $response = $backend->configdRun("acmeclient upload-sftp ${cert_id} ${action_id}"); - break; - case 'configd': - // Make sure a configd command was specified. - if (empty((string)$action->configd)) { - log_error("AcmeClient: no configd command specified for automation: " . $action->name); - $result = '1'; - } else { - $response = $backend->configdRun((string)$action->configd); - } - break; - default: - log_error("AcmeClient: an invalid automation was specified: " . (string)$action->type); - $return = 1; - break; - } - } - } - - 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) -{ - global $postponed_updates; - - $uuid = $certObj->attributes()->uuid; - $node = $modelObj->getNodeByReference('certificates.certificate.' . $uuid); - if ($node != null) { - $postponed_updates[] = array( - 'uuid' => (string)$uuid, - 'statusCode' => $statusCode, - 'statusLastUpdate' => time()); - } else { - log_error("AcmeClient: unable to update acme status for certificate " . (string)$certObj->name); - return(1); - } -} - -/* Write postponed certificate status updates to the configuration. - * This workaround seems to fix the "Node no longer exists" error - * that haunted us for quite some time. -*/ -function dump_postponed_updates() -{ - global $postponed_updates; - - $status_descr = [ - 100 => 'unknown', - 200 => 'OK', - 250 => 'cert revoked', - 300 => 'configuration error', - 400 => 'validation failed', - 500 => 'internal error', - ]; - - $modelObj = new OPNsense\AcmeClient\AcmeClient(); - - foreach ($postponed_updates as $pupdate) { - $_statusCode = $pupdate['statusCode']; - $_uuid = $pupdate['uuid']; - $node = $modelObj->getNodeByReference('certificates.certificate.' . $_uuid); - if ($node != null) { - log_error("AcmeClient: storing status '" . $status_descr[$_statusCode] . "' for cert " . (string)$node->name); - $node->statusCode = $_statusCode; - $node->statusLastUpdate = $pupdate['statusLastUpdate']; - // serialize to config and save - $modelObj->serializeToConfig(); - Config::getInstance()->save(); - Config::getInstance()->forceReload(); - } else { - log_error(sprintf("AcmeClient: failed to store status '%s' for cert %s: node not found", $status_descr[$_statusCode], $_uuid)); - } - } -} - -function run_shell_command($proc_cmd, $proc_env = array()) -{ - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - $proc = proc_open($proc_cmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - log_error(sprintf("AcmeClient: The shell command '%s' returned exit code '%d'", $proc_cmd, $result)); - return($result); - } else { - log_error(sprintf("AcmeClient: Unable to prepare shell command '%s'", $proc_cmd)); - return(1); - } -} - -// taken from certs.inc -function local_cert_get_subject_array($str_crt, $decode = true) -{ - if ($decode) { - $str_crt = base64_decode($str_crt); - } - $inf_crt = openssl_x509_parse($str_crt); - $components = $inf_crt['subject']; - - if (!is_array($components)) { - return; - } - - $subject_array = array(); - - foreach ($components as $a => $v) { - $subject_array[] = array('a' => $a, 'v' => $v); - } - - return $subject_array; -} - -// taken from certs.inc -function local_cert_get_cn($crt, $decode = true) -{ - $sub = local_cert_get_subject_array($crt, $decode); - if (is_array($sub)) { - foreach ($sub as $s) { - if (strtoupper($s['a']) == "CN") { - return $s['v']; - } - } - } - return ""; -} - -// taken from system_camanager.php -function local_ca_import(&$ca, $str, $key = "", $serial = 0) -{ - global $config; - - $ca['crt'] = base64_encode($str); - if (!empty($key)) { - $ca['prv'] = base64_encode($key); - } - if (!empty($serial)) { - $ca['serial'] = $serial; - } - $subject = cert_get_subject($str, false); - $issuer = cert_get_issuer($str, false); - - // Find my issuer unless self-signed - if ($issuer != $subject) { - $issuer_crt =& lookup_ca_by_subject($issuer); - if ($issuer_crt) { - $ca['caref'] = $issuer_crt['refid']; - } - } - - /* Correct if child certificate was loaded first */ - if (is_array($config['ca'])) { - foreach ($config['ca'] as & $oca) { - $issuer = cert_get_issuer($oca['crt']); - if ($ca['refid'] != $oca['refid'] && $issuer == $subject) { - $oca['caref'] = $ca['refid']; - } - } - } - if (is_array($config['cert'])) { - foreach ($config['cert'] as & $cert) { - $issuer = cert_get_issuer($cert['crt']); - if ($issuer == $subject) { - $cert['caref'] = $ca['refid']; - } - } - } - return true; -} - -function base64url_encode($str) -{ - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); -} -function base64url_decode($str) -{ - return base64_decode(str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT)); -} - -exit; diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php new file mode 100755 index 000000000..0b47d933d --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php @@ -0,0 +1,193 @@ +#!/usr/local/bin/php + [ + 'description' => 'issue or renew certificates', + ], + 'import' => [ + 'description' => 're-import certificate into trust store', + ], + 'revoke' => [ + 'description' => 'revoke the specified certificate', + ], + 'remove' => [ + 'description' => 'remove all files and configuration for the specified certificate', + ], + 'reset' => [ + 'description' => 'reset the specified certificate by removing it\'s private key', + ], + 'automation' => [ + 'description' => 'run automations for the specified certificate', + ], + 'register' => [ + 'description' => 'register the specified account with Lets Encrypt', + ], +]; + +// Supported command line options and their usage information. +const STATIC_OPTIONS = << $options) { + echo "\"$name\" - {$options["description"]}" . PHP_EOL; + } + + echo PHP_EOL . "Examples:" . PHP_EOL + . str_replace('/\r\n|\n|\r/g', PHP_EOL, EXAMPLES) + . PHP_EOL . PHP_EOL; +} + +/** + * check if the specified mode is supported + */ +function validateMode($mode) +{ + $return = false; + foreach (MODES as $name => $options) { + if ($mode === $name) { + $return = true; + break; + } + } + return $return; +} + +function main() +{ + // Parse command line arguments + $options = getopt('h', ['account:', 'all', 'cert:', 'force', 'help', 'mode:']); + $force = isset($options['force']) ? true : false; + + // Verify mode and arguments + if (empty($options) || isset($options['h']) || isset($options['help']) || + (isset($options['mode']) and !validateMode($options['mode']))) { + // Not enough or invalid arguments specified. + help(); + } elseif (($options['mode'] === 'issue') && (isset($options['cert']) || isset($options['all']))) { + // Work on all or only on a single certificate + if (isset($options['all'])) { + // Iterate over all certificates + $config = OPNsense\Core\Config::getInstance()->object(); + $acme = $config->OPNsense->AcmeClient; + + // Iterate over all certificates + foreach ($acme->certificates->children() as $certCfg) { + $cert_uuid = (string)$certCfg->attributes()['uuid']; + $cert = new LeCertificate($cert_uuid, $force); + // NOTE: Disabled certificates are automatically ignored by LeCertificate. + $cert->issue(); + } + } else { + // NOTE: Disabled certificates are automatically ignored by LeCertificate. + $cert = new LeCertificate($options['cert'], $force); + $cert->issue(); + } + } elseif ($options['mode'] === 'import' && isset($options['cert'])) { + $cert = new LeCertificate($options['cert']); + $cert->import(); + } elseif ($options['mode'] === 'revoke' && isset($options['cert'])) { + $cert = new LeCertificate($options['cert']); + $cert->revoke(); + } elseif ($options['mode'] === 'remove' && isset($options['cert'])) { + $cert = new LeCertificate($options['cert']); + $cert->remove(); + } elseif ($options['mode'] === 'reset' && isset($options['cert'])) { + $cert = new LeCertificate($options['cert']); + $cert->reset(); + } elseif ($options['mode'] === 'automation' && isset($options['cert'])) { + $cert = new LeCertificate($options['cert']); + $cert->runAutomations(); + } elseif ($options['mode'] === 'register' && isset($options['account'])) { + $account = new LeAccount($options['account']); + $account->register(); + } else { + // Fallback to help + help(); + } +} + +// Run! +main(); 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 6122512e0..1b9069640 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 @@ -42,48 +42,54 @@ message:testing acme_http_challenge configuration ########################################## [sign-cert] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -F -a sign -c +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --force --cert parameters:%s type:script message:signing or renewing a certificate [revoke-cert] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a revoke -c +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode revoke --cert parameters:%s type:script message:revoking a certificate [remove-cert] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a remove -c +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode remove --cert parameters:%s type:script message:removing a certificate [remove-key] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a removekey -c +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode reset --cert parameters:%s type:script message:removing a certificate private key [sign-all-certs] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a sign -A +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --all parameters: type:script -message:signing or renewing a certificate +message:signing or renewing all certificates [run-automation] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a automation -c +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode automation --cert parameters:%s type:script message:running automations for a certificate [cron-auto-renew] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a sign -A -C +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --all parameters: type:script message:cronjob running to sign or renew certificates description:Renew Let's Encrypt certificates +[register-account] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode register --account +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