From fc3fad5e87884d73f00fa6900f16280969c3c18d Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Mon, 4 Mar 2024 13:48:00 +0100 Subject: [PATCH] check-certificates: move code into function --- check-certificates.rsc | 347 +++++++++++++++++++++-------------------- 1 file changed, 176 insertions(+), 171 deletions(-) diff --git a/check-certificates.rsc b/check-certificates.rsc index 1487a3e..f15f145 100644 --- a/check-certificates.rsc +++ b/check-certificates.rsc @@ -8,202 +8,207 @@ # check for certificate validity # https://git.eworm.de/cgit/routeros-scripts/about/doc/check-certificates.md -:local 0 [ :jobname ]; :global GlobalFunctionsReady; :while ($GlobalFunctionsReady != true) do={ :delay 500ms; } -:global CertRenewTime; -:global CertRenewUrl; -:global CertWarnTime; -:global Identity; - -:global CertificateAvailable -:global EscapeForRegEx; -:global IfThenElse; -:global LogPrintExit2; -:global ParseKeyValueStore; -:global ScriptLock; -:global SendNotification2; -:global SymbolForNotification; -:global UrlEncode; -:global WaitFullyConnected; - -:local CheckCertificatesDownloadImport do={ - :local Name [ :tostr $1 ]; +:local Main do={ + :local ScriptName [ :tostr $1 ]; + :global CertRenewTime; :global CertRenewUrl; - :global CertRenewPass; + :global CertWarnTime; + :global Identity; - :global CertificateNameByCN; + :global CertificateAvailable :global EscapeForRegEx; - :global FetchUserAgent; - :global LogPrintExit2; - :global UrlEncode; - :global WaitForFile; - - :local Return false; - - :foreach Type in={ ".pem"; ".p12" } do={ - :local CertFileName ([ $UrlEncode $Name ] . $Type); - :do { - /tool/fetch check-certificate=yes-without-crl http-header-field=({ $FetchUserAgent }) \ - ($CertRenewUrl . $CertFileName) dst-path=$CertFileName as-value; - $WaitForFile $CertFileName; - - :local DecryptionFailed true; - :foreach PassPhrase in=$CertRenewPass do={ - :local Result [ /certificate/import file-name=$CertFileName passphrase=$PassPhrase as-value ]; - :if ($Result->"decryption-failures" = 0) do={ - :set DecryptionFailed false; - } - } - /file/remove [ find where name=$CertFileName ]; - - :if ($DecryptionFailed = true) do={ - $LogPrintExit2 warning $0 ("Decryption failed for certificate file '" . $CertFileName . "'.") false; - } - - :foreach CertInChain in=[ /certificate/find where name~("^" . [ $EscapeForRegEx $CertFileName ] . "_[0-9]+\$") \ - common-name!=$Name !(subject-alt-name~("(^|\\W)(DNS|IP):" . [ $EscapeForRegEx $Name ] . "(\\W|\$)")) !(common-name=[]) ] do={ - $CertificateNameByCN [ /certificate/get $CertInChain common-name ]; - } - - :set Return true; - } on-error={ - $LogPrintExit2 debug $0 ("Could not download certificate file '" . $CertFileName . "'.") false; - } - } - - :return $Return; -} - -:local FormatInfo do={ - :local Cert $1; - - :global FormatLine; - :global FormatMultiLines; :global IfThenElse; + :global LogPrintExit2; + :global ParseKeyValueStore; + :global ScriptLock; + :global SendNotification2; + :global SymbolForNotification; + :global UrlEncode; + :global WaitFullyConnected; - :local FormatExpire do={ - :global CharacterReplace; - :return [ $CharacterReplace [ $CharacterReplace [ :tostr $1 ] "w" "w " ] "d" "d " ]; + :local CheckCertificatesDownloadImport do={ + :local Name [ :tostr $1 ]; + + :global CertRenewUrl; + :global CertRenewPass; + + :global CertificateNameByCN; + :global EscapeForRegEx; + :global FetchUserAgent; + :global LogPrintExit2; + :global UrlEncode; + :global WaitForFile; + + :local Return false; + + :foreach Type in={ ".pem"; ".p12" } do={ + :local CertFileName ([ $UrlEncode $Name ] . $Type); + :do { + /tool/fetch check-certificate=yes-without-crl http-header-field=({ $FetchUserAgent }) \ + ($CertRenewUrl . $CertFileName) dst-path=$CertFileName as-value; + $WaitForFile $CertFileName; + + :local DecryptionFailed true; + :foreach PassPhrase in=$CertRenewPass do={ + :local Result [ /certificate/import file-name=$CertFileName passphrase=$PassPhrase as-value ]; + :if ($Result->"decryption-failures" = 0) do={ + :set DecryptionFailed false; + } + } + /file/remove [ find where name=$CertFileName ]; + + :if ($DecryptionFailed = true) do={ + $LogPrintExit2 warning $0 ("Decryption failed for certificate file '" . $CertFileName . "'.") false; + } + + :foreach CertInChain in=[ /certificate/find where name~("^" . [ $EscapeForRegEx $CertFileName ] . "_[0-9]+\$") \ + common-name!=$Name !(subject-alt-name~("(^|\\W)(DNS|IP):" . [ $EscapeForRegEx $Name ] . "(\\W|\$)")) !(common-name=[]) ] do={ + $CertificateNameByCN [ /certificate/get $CertInChain common-name ]; + } + + :set Return true; + } on-error={ + $LogPrintExit2 debug $0 ("Could not download certificate file '" . $CertFileName . "'.") false; + } + } + + :return $Return; } - :local FormatCertChain do={ + :local FormatInfo do={ :local Cert $1; - :global EitherOr; - :global ParseKeyValueStore; + :global FormatLine; + :global FormatMultiLines; + :global IfThenElse; + + :local FormatExpire do={ + :global CharacterReplace; + :return [ $CharacterReplace [ $CharacterReplace [ :tostr $1 ] "w" "w " ] "d" "d " ]; + } + + :local FormatCertChain do={ + :local Cert $1; + + :global EitherOr; + :global ParseKeyValueStore; + + :local CertVal [ /certificate/get $Cert ]; + :local Return ""; + + :for I from=0 to=5 do={ + :set Return ($Return . [ $EitherOr ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") \ + ([ $ParseKeyValueStore (($CertVal->"issuer")->0) ]->"CN") ]); + :set CertVal [ /certificate/get [ find where skid=($CertVal->"akid") ] ]; + :if (($CertVal->"akid") = "" || ($CertVal->"akid") = ($CertVal->"skid")) do={ + :return $Return; + } + :set Return ($Return . " -> "); + } + :return ($Return . "..."); + } :local CertVal [ /certificate/get $Cert ]; - :local Return ""; - :for I from=0 to=5 do={ - :set Return ($Return . [ $EitherOr ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") \ - ([ $ParseKeyValueStore (($CertVal->"issuer")->0) ]->"CN") ]); - :set CertVal [ /certificate/get [ find where skid=($CertVal->"akid") ] ]; - :if (($CertVal->"akid") = "" || ($CertVal->"akid") = ($CertVal->"skid")) do={ - :return $Return; - } - :set Return ($Return . " -> "); - } - :return ($Return . "..."); + :return ( \ + [ $FormatLine "Name" ($CertVal->"name") ] . "\n" . \ + [ $IfThenElse ([ :len ($CertVal->"common-name") ] > 0) ([ $FormatLine "CommonName" ($CertVal->"common-name") ] . "\n") ] . \ + [ $IfThenElse ([ :len ($CertVal->"subject-alt-name") ] > 0) ([ $FormatMultiLines "SubjectAltNames" ($CertVal->"subject-alt-name") ] . "\n") ] . \ + [ $FormatLine "Private key" [ $IfThenElse (($CertVal->"private-key") = true) "available" "missing" ] ] . "\n" . \ + [ $FormatLine "Fingerprint" ($CertVal->"fingerprint") ] . "\n" . \ + [ $IfThenElse ([ :len ($CertVal->"ca") ] > 0) [ $FormatLine "Issuer" ($CertVal->"ca") ] [ $FormatLine "Issuer chain" [ $FormatCertChain $Cert ] ] ] . "\n" . \ + "Validity:\n" . \ + [ $FormatLine " from" ($CertVal->"invalid-before") ] . "\n" . \ + [ $FormatLine " to" ($CertVal->"invalid-after") ] . "\n" . \ + [ $FormatLine "Expires in" [ $IfThenElse (($CertVal->"expired") = true) "expired" [ $FormatExpire ($CertVal->"expires-after") ] ] ]); } - :local CertVal [ /certificate/get $Cert ]; + $ScriptLock $ScriptName; + $WaitFullyConnected; - :return ( \ - [ $FormatLine "Name" ($CertVal->"name") ] . "\n" . \ - [ $IfThenElse ([ :len ($CertVal->"common-name") ] > 0) ([ $FormatLine "CommonName" ($CertVal->"common-name") ] . "\n") ] . \ - [ $IfThenElse ([ :len ($CertVal->"subject-alt-name") ] > 0) ([ $FormatMultiLines "SubjectAltNames" ($CertVal->"subject-alt-name") ] . "\n") ] . \ - [ $FormatLine "Private key" [ $IfThenElse (($CertVal->"private-key") = true) "available" "missing" ] ] . "\n" . \ - [ $FormatLine "Fingerprint" ($CertVal->"fingerprint") ] . "\n" . \ - [ $IfThenElse ([ :len ($CertVal->"ca") ] > 0) [ $FormatLine "Issuer" ($CertVal->"ca") ] [ $FormatLine "Issuer chain" [ $FormatCertChain $Cert ] ] ] . "\n" . \ - "Validity:\n" . \ - [ $FormatLine " from" ($CertVal->"invalid-before") ] . "\n" . \ - [ $FormatLine " to" ($CertVal->"invalid-after") ] . "\n" . \ - [ $FormatLine "Expires in" [ $IfThenElse (($CertVal->"expired") = true) "expired" [ $FormatExpire ($CertVal->"expires-after") ] ] ]); -} + :foreach Cert in=[ /certificate/find where !revoked !ca !scep-url expires-after<$CertRenewTime ] do={ + :local CertVal [ /certificate/get $Cert ]; + :local CertNew; + :local LastName; -$ScriptLock $0; -$WaitFullyConnected; - -:foreach Cert in=[ /certificate/find where !revoked !ca !scep-url expires-after<$CertRenewTime ] do={ - :local CertVal [ /certificate/get $Cert ]; - :local CertNew; - :local LastName; - - :do { - :if ([ :len $CertRenewUrl ] = 0) do={ - $LogPrintExit2 info $0 ("No CertRenewUrl given.") true; - } - $LogPrintExit2 info $0 ("Attempting to renew certificate '" . ($CertVal->"name") . "'.") false; - - :local ImportSuccess false; - :set LastName ($CertVal->"common-name"); - :set ImportSuccess [ $CheckCertificatesDownloadImport $LastName ]; - :foreach SAN in=($CertVal->"subject-alt-name") do={ - :if ($ImportSuccess = false) do={ - :set LastName [ :pick $SAN ([ :find $SAN ":" ] + 1) [ :len $SAN ] ]; - :set ImportSuccess [ $CheckCertificatesDownloadImport $LastName ]; + :do { + :if ([ :len $CertRenewUrl ] = 0) do={ + $LogPrintExit2 info $ScriptName ("No CertRenewUrl given.") true; } - } + $LogPrintExit2 info $ScriptName ("Attempting to renew certificate '" . ($CertVal->"name") . "'.") false; - :if ([ :len ($CertVal->"fingerprint") ] > 0 && $CertVal->"fingerprint" != [ /certificate/get $Cert fingerprint ]) do={ - $LogPrintExit2 debug $0 ("Certificate '" . $CertVal->"name" . "' was updated in place.") false; - :set CertVal [ /certificate/get $Cert ]; + :local ImportSuccess false; + :set LastName ($CertVal->"common-name"); + :set ImportSuccess [ $CheckCertificatesDownloadImport $LastName ]; + :foreach SAN in=($CertVal->"subject-alt-name") do={ + :if ($ImportSuccess = false) do={ + :set LastName [ :pick $SAN ([ :find $SAN ":" ] + 1) [ :len $SAN ] ]; + :set ImportSuccess [ $CheckCertificatesDownloadImport $LastName ]; + } + } + + :if ([ :len ($CertVal->"fingerprint") ] > 0 && $CertVal->"fingerprint" != [ /certificate/get $Cert fingerprint ]) do={ + $LogPrintExit2 debug $ScriptName ("Certificate '" . $CertVal->"name" . "' was updated in place.") false; + :set CertVal [ /certificate/get $Cert ]; + } else={ + $LogPrintExit2 debug $ScriptName ("Certificate '" . $CertVal->"name" . "' was not updated, but replaced.") false; + + :set CertNew [ /certificate/find where name~("^" . [ $EscapeForRegEx [ $UrlEncode $LastName ] ] . "\\.(p12|pem)_[0-9]+\$") \ + (common-name=($CertVal->"common-name") or subject-alt-name~("(^|\\W)(DNS|IP):" . [ $EscapeForRegEx $LastName ] . "(\\W|\$)")) \ + fingerprint!=[ :tostr ($CertVal->"fingerprint") ] expires-after>$CertRenewTime ]; + :local CertNewVal [ /certificate/get $CertNew ]; + + :if ([ $CertificateAvailable ([ $ParseKeyValueStore ($CertNewVal->"issuer") ]->"CN") ] = false) do={ + $LogPrintExit2 warning $ScriptName ("The certificate chain is not available!") false; + } + + :if (($CertVal->"private-key") = true && ($CertVal->"private-key") != ($CertNewVal->"private-key")) do={ + /certificate/remove $CertNew; + $LogPrintExit2 warning $ScriptName ("Old certificate '" . ($CertVal->"name") . "' has a private key, new certificate does not. Aborting renew.") true; + } + + /ip/service/set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ]; + + /ip/ipsec/identity/set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ]; + /ip/ipsec/identity/set remote-certificate=($CertNewVal->"name") [ find where remote-certificate=($CertVal->"name") ]; + + /ip/hotspot/profile/set ssl-certificate=($CertNewVal->"name") [ find where ssl-certificate=($CertVal->"name") ]; + + /certificate/remove $Cert; + /certificate/set $CertNew name=($CertVal->"name"); + :set CertNewVal; + :set CertVal [ /certificate/get $CertNew ]; + } + + $SendNotification2 ({ origin=$ScriptName; silent=true; \ + subject=([ $SymbolForNotification "lock-with-ink-pen" ] . "Certificate renewed: " . ($CertVal->"name")); \ + message=("A certificate on " . $Identity . " has been renewed.\n\n" . [ $FormatInfo $CertNew ]) }); + $LogPrintExit2 info $ScriptName ("The certificate '" . ($CertVal->"name") . "' has been renewed.") false; + } on-error={ + $LogPrintExit2 debug $ScriptName ("Could not renew certificate '" . ($CertVal->"name") . "'.") false; + } + } + + :foreach Cert in=[ /certificate/find where !revoked !scep-url !(expires-after=[]) \ + expires-after<$CertWarnTime !(fingerprint=[]) ] do={ + :local CertVal [ /certificate/get $Cert ]; + + :if ([ :len [ /certificate/scep-server/find where ca-cert=($CertVal->"ca") ] ] > 0) do={ + $LogPrintExit2 debug $ScriptName ("Certificate '" . ($CertVal->"name") . "' is handled by SCEP, skipping.") false; } else={ - $LogPrintExit2 debug $0 ("Certificate '" . $CertVal->"name" . "' was not updated, but replaced.") false; + :local State [ $IfThenElse (($CertVal->"expired") = true) "expired" "is about to expire" ]; - :set CertNew [ /certificate/find where name~("^" . [ $EscapeForRegEx [ $UrlEncode $LastName ] ] . "\\.(p12|pem)_[0-9]+\$") \ - (common-name=($CertVal->"common-name") or subject-alt-name~("(^|\\W)(DNS|IP):" . [ $EscapeForRegEx $LastName ] . "(\\W|\$)")) \ - fingerprint!=[ :tostr ($CertVal->"fingerprint") ] expires-after>$CertRenewTime ]; - :local CertNewVal [ /certificate/get $CertNew ]; - - :if ([ $CertificateAvailable ([ $ParseKeyValueStore ($CertNewVal->"issuer") ]->"CN") ] = false) do={ - $LogPrintExit2 warning $0 ("The certificate chain is not available!") false; - } - - :if (($CertVal->"private-key") = true && ($CertVal->"private-key") != ($CertNewVal->"private-key")) do={ - /certificate/remove $CertNew; - $LogPrintExit2 warning $0 ("Old certificate '" . ($CertVal->"name") . "' has a private key, new certificate does not. Aborting renew.") true; - } - - /ip/service/set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ]; - - /ip/ipsec/identity/set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ]; - /ip/ipsec/identity/set remote-certificate=($CertNewVal->"name") [ find where remote-certificate=($CertVal->"name") ]; - - /ip/hotspot/profile/set ssl-certificate=($CertNewVal->"name") [ find where ssl-certificate=($CertVal->"name") ]; - - /certificate/remove $Cert; - /certificate/set $CertNew name=($CertVal->"name"); - :set CertNewVal; - :set CertVal [ /certificate/get $CertNew ]; + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "warning-sign" ] . "Certificate warning: " . ($CertVal->"name")); \ + message=("A certificate on " . $Identity . " " . $State . ".\n\n" . [ $FormatInfo $Cert ]) }); + $LogPrintExit2 info $ScriptName ("The certificate '" . ($CertVal->"name") . "' " . $State . \ + ", it is invalid after " . ($CertVal->"invalid-after") . ".") false; } - - $SendNotification2 ({ origin=$0; silent=true; \ - subject=([ $SymbolForNotification "lock-with-ink-pen" ] . "Certificate renewed: " . ($CertVal->"name")); \ - message=("A certificate on " . $Identity . " has been renewed.\n\n" . [ $FormatInfo $CertNew ]) }); - $LogPrintExit2 info $0 ("The certificate '" . ($CertVal->"name") . "' has been renewed.") false; - } on-error={ - $LogPrintExit2 debug $0 ("Could not renew certificate '" . ($CertVal->"name") . "'.") false; } } -:foreach Cert in=[ /certificate/find where !revoked !scep-url !(expires-after=[]) \ - expires-after<$CertWarnTime !(fingerprint=[]) ] do={ - :local CertVal [ /certificate/get $Cert ]; - - :if ([ :len [ /certificate/scep-server/find where ca-cert=($CertVal->"ca") ] ] > 0) do={ - $LogPrintExit2 debug $0 ("Certificate '" . ($CertVal->"name") . "' is handled by SCEP, skipping.") false; - } else={ - :local State [ $IfThenElse (($CertVal->"expired") = true) "expired" "is about to expire" ]; - - $SendNotification2 ({ origin=$0; \ - subject=([ $SymbolForNotification "warning-sign" ] . "Certificate warning: " . ($CertVal->"name")); \ - message=("A certificate on " . $Identity . " " . $State . ".\n\n" . [ $FormatInfo $Cert ]) }); - $LogPrintExit2 info $0 ("The certificate '" . ($CertVal->"name") . "' " . $State . \ - ", it is invalid after " . ($CertVal->"invalid-after") . ".") false; - } -} +$Main [ :jobname ];