#!/bin/sh # # check_ssl_cert # Checks an X.509 certificate # see https://github.com/matteocorti/check_ssl_cert # # See the INSTALL.md file for installation instructions # # Copyright (c) 2007-2012 ETH Zurich # Copyright (c) 2007-2024 Matteo Corti # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ################################################################################ # Constants VERSION=2.81.0 SHORTNAME="SSL_CERT" VALID_ATTRIBUTES=",startdate,enddate,subject,issuer,modulus,serial,hash,email,ocsp_uri,fingerprint," SIGNALS="HUP INT QUIT TERM ABRT" LC_ALL=C ################################################################################ # Variables ALL_MSG="" CONFIGURATION_FILE="${HOME}/.check_ssl_certrc" CRITICAL_MSG="" DEBUG=0 DEBUG_FILE="" DEFAULT_REQUIRED_HTTP_HEADERS="strict-transport-security,X-Frame-Options,Content-Security-Policy,X-Content-Type-Options,Referrer-Policy,Permissions-Policy" DEFAULT_UNREQUIRED_HTTP_HEADERS="X-Powered-By,X-Aspnet-Version,X-XSS-Protection,X-AspNetMvc-Version" EARLIEST_VALIDITY_HOURS="" HOST_CACHE="${HOME}/.check_ssl_cert-cache" REQUIRED_HTTP_HEADERS="" UNREQUIRED_HTTP_HEADERS="" HTTP_HEADERS_PATH="/" STATUS_CRITICAL=2 STATUS_OK=0 STATUS_UNKNOWN=3 STATUS_WARNING=1 TEMPFILE="" WARNING_MSG="" FINGERPRINT_ALG=sha1 DEFAULT_FORMAT="%SHORTNAME% %STATUS% - %HOST%:%PORT%, %PROTOCOL%, %OPENSSL_COMMAND% %SELFSIGNEDCERT%certificate %DISPLAY_CN%%CHECKEDNAMES%from '%CA_ISSUER_MATCHED%' valid until %DATE%%DAYS_VALID%%OCSP_EXPIRES_IN_HOURS%%SSL_LABS_HOST_GRADE%" # if --critical or --warning are floating point then switch to floating point output, otherwise integer # Floating point precision: default integer SCALE="" ################################################################################ # Functions # substituted the variables defined by the --format command line option format_template() { string=$1 debuglog "output parameters: STATUS = ${STATUS}" debuglog "output parameters: CA_ISSUER_MATCHED = ${CA_ISSUER_MATCHED}" debuglog "output parameters: CHECKEDNAMES = ${CHECKEDNAMES}" debuglog "output parameters: CN = ${CN}" debuglog "output parameters: DATE = ${DATE}" debuglog "output parameters: DAYS_VALID = ${DAYS_VALID}" debuglog "output parameters: DYSPLAY_CN = ${DISPLAY_CN}" debuglog "output parameters: OPENSSL_COMMAND = ${OPENSSL_COMMAND}" debuglog "output parameters: SELFSIGNEDCERT = ${SELFSIGNEDCERT}" debuglog "output parameters: SHORTNAME = ${SHORTNAME}" debuglog "output parameters: OCSP_EXPIRES_IN_HOURS = ${OCSP_EXPIRES_IN_HOURS}" debuglog "output parameters: SSL_LABS_HOST_GRADE = ${SSL_LABS_HOST_GRADE}" debuglog "output parameters: PROTOCOL = ${PROTOCOL}" STATUS_TMP="$(var_for_sed STATUS "${STATUS}")" CA_ISSUER_MATCHED_TMP="$(var_for_sed CA_ISSUER_MATCHED "${CA_ISSUER_MATCHED}")" CHECKEDNAMES_TMP="$(var_for_sed CHECKEDNAMES "${CHECKEDNAMES}")" CN_TMP="$(var_for_sed CN "${CN}")" DATE_TMP="$(var_for_sed DATE "${DATE}")" DAYS_VALID_TMP="$(var_for_sed DAYS_VALID "${DAYS_VALID}")" DISPLAY_CN_TMP="$(var_for_sed DISPLAY_CN "${DISPLAY_CN}")" HOST_TMP="$(var_for_sed HOST "${HOST}")" OCSP_EXPIRES_IN_HOURS_TMP="$(var_for_sed OCSP_EXPIRES_IN_HOURS "${OCSP_EXPIRES_IN_HOURS}")" OPENSSL_COMMAND_TMP="$(var_for_sed OPENSSL_COMMAND "${OPENSSL_COMMAND}")" PORT_TMP="$(var_for_sed PORT "${PORT}")" PROTOCOL_TMP="$(var_for_sed PROTOCOL "${PROTOCOL}")" SELFSIGNEDCERT_TMP="$(var_for_sed SELFSIGNEDCERT "${SELFSIGNEDCERT}")" SHORTNAME_TMP="$(var_for_sed SHORTNAME "${SHORTNAME}")" SIGALGO_TMP="$(var_for_sed SIGALGO "${PUB_KEY_ALGORITHM}")" SSL_LABS_HOST_GRADE_TMP="$(var_for_sed SSL_LABS_HOST_GRADE "${SSL_LABS_HOST_GRADE}")" echo "${string}" | sed \ -e "${STATUS_TMP}" \ -e "${CA_ISSUER_MATCHED_TMP}" \ -e "${CHECKEDNAMES_TMP}" \ -e "${CN_TMP}" \ -e "${DATE_TMP}" \ -e "${DAYS_VALID_TMP}" \ -e "${DISPLAY_CN_TMP}" \ -e "${HOST_TMP}" \ -e "${OCSP_EXPIRES_IN_HOURS_TMP}" \ -e "${OPENSSL_COMMAND_TMP}" \ -e "${PORT_TMP}" \ -e "${PROTOCOL_TMP}" \ -e "${SELFSIGNEDCERT_TMP}" \ -e "${SHORTNAME_TMP}" \ -e "${SIGALGO_TMP}" \ -e "${SSL_LABS_HOST_GRADE_TMP}" } ################################################################################ # Add the specified header to the list of required HTTP headers # Usage: # add_required_header X-Frame-Options add_required_header() { header=$1 debuglog "Adding ${header} to the list of required HTTP headers: ${REQUIRED_HTTP_HEADERS}" if [ -z "${REQUIRED_HTTP_HEADERS}" ]; then REQUIRED_HTTP_HEADERS="${header}" else REQUIRED_HTTP_HEADERS="${REQUIRED_HTTP_HEADERS},${header}" fi } ################################################################################ # Add the specified header to the list of unrequired HTTP headers # Usage: # add_unrequired_header X-Powered-By add_unrequired_header() { header=$1 debuglog "Adding ${header} to the list of unrequired HTTP headers: ${UNREQUIRED_HTTP_HEADERS}" if [ -z "${UNREQUIRED_HTTP_HEADERS}" ]; then UNREQUIRED_HTTP_HEADERS="${header}" else UNREQUIRED_HTTP_HEADERS="${UNREQUIRED_HTTP_HEADERS},${header}" fi } CACHED_HEADERS= fetch_http_headers() { if [ -z "${CACHED_HEADERS}" ]; then TIMEOUT_REASON='Fetching HTTP headers' debuglog "Fetching headers" create_temporary_file CACHED_HEADERS=${TEMPFILE} CURL_RESOLVE='' if [ -n "${RESOLVE}" ]; then CURL_RESOLVE="--resolve ${HOST}:${PORT}:${RESOLVE}" fi # curl options: # -s (--silent) # -D (--dump-header) # -A (--user-agent) # -L (--location): follow redirects # -k (--insecure): ignore TLS problems (we want to check the headers anyway) # -o (--output): discard the output (we are only interested in the HTTP headers) exec_with_timeout "${CURL_BIN} ${CURL_QUIC} ${CURL_PROXY} ${CURL_RESOLVE} ${CURL_PROXY_ARGUMENT} ${INETPROTO} -k -s -D- -A '${HTTP_USER_AGENT}' -o /dev/null -L https://${HOST}${path}" "${CACHED_HEADERS}" RET=$? if [ "${RET}" -ne 0 ]; then debuglog "Cannot retrieve HTTP headers (curl error code: ${RET})" prepend_critical_message "Cannot retrieve HTTP headers" fi if [ "${DEBUG}" -gt 1 ]; then ESCAPED_PATH=$(echo "${path}" | sed 's/\//\\\//g') # there might be empty lines "${GREP_BIN}" '[[:alpha:]]' "${CACHED_HEADERS}" | sed "s/^/[DBG] HTTP headers for https:\\/\\/${HOST}${ESCAPED_PATH}: /" 1>&2 fi if [ -n "${DEBUG_HEADERS}" ]; then cp "${CACHED_HEADERS}" headers.txt fi unset TIMEOUT_REASON fi } check_required_http_header() { header=$1 path=$2 fetch_http_headers debuglog "Checking required header '${header}' with path '${path}'" if ! "${GREP_BIN}" -q -i "^${header}:" "${CACHED_HEADERS}"; then debuglog "Required header '${header}' not found" prepend_critical_message "HTTP header '${header}' is not supported" else HEADER_VALUE=$("${GREP_BIN}" -i "^${header}:" "${CACHED_HEADERS}" | sed 's/[^:]*: //' | tr -d '\n' | tr -d '\r') debuglog "Required header '${header}' found (${HEADER_VALUE})" verboselog "Required header '${header}' is supported (${HEADER_VALUE})" fi } check_unrequired_http_header() { header=$1 path=$2 fetch_http_headers debuglog "Checking unrequired header '${header}' with path '${path}'" if "${GREP_BIN}" -q -i "^${header}:" "${CACHED_HEADERS}"; then HEADER_VALUE=$("${GREP_BIN}" -i "^${header}:" "${CACHED_HEADERS}" | sed 's/[^:]*: //' | tr -d '\n' | tr -d '\r') debuglog "Unwanted header '${header}' found (${HEADER_VALUE})" prepend_critical_message "HTTP header '${header}' is supported (${HEADER_VALUE})" else debuglog "Unwanted header '${header}' not found" verboselog "Unwanted header '${header}' is not supported" fi } ################################################################################ # To set a variable with an HEREDOC in a POSIX compliant way # see: https://unix.stackexchange.com/questions/340718/how-do-i-bring-heredoc-text-into-a-shell-script-variable # Usage: # set_variable variablename<<'HEREDOC' # ... # HEREDOC set_variable() { # shellcheck disable=SC2016 eval "$1"'=$(cat)' } ################################################################################ # Prints usage information # Params # $1 error message (optional) usage() { echo echo "Usage: check_ssl_cert -H host [OPTIONS]" echo " check_ssl_cert -f file [OPTIONS]" echo echo "Arguments:" echo " -f,--file file Local file path or URI." echo " With -f you can not only pass a x509" echo " certificate file but also a certificate" echo " revocation list (CRL) to check the" echo " validity period or a Java KeyStore file" echo " -H,--host host Server" echo echo "Options:" # Delimiter at 78 chars ############################################################ echo " -A,--noauth Ignore authority warnings (expiration" echo " only)" echo " --all Enable all the possible optional checks" echo " at the maximum level" echo " --all-local Enable all the possible optional checks" echo " at the maximum level (without SSL-Labs)" echo " --allow-empty-san Allow certificates without Subject" echo " Alternative Names (SANs)" # Delimiter at 78 chars ############################################################ echo " -C,--clientcert path Use client certificate to authenticate" echo " -c,--critical days Minimum number of days a certificate has" echo " to be valid to issue a critical status." echo " Can be a floating point number, e.g., 0.5" echo " Default: ${CRITICAL_DAYS}" echo " --check-chain The certificate chain cannot contain" echo " double or root certificates" echo " --check-ciphers grade Check the offered ciphers" echo " --check-ciphers-warnings Critical if nmap reports a warning for an" echo " offered cipher" echo " --check-http-headers Check the HTTP headers for best practices" echo " --check-ssl-labs-warn grade SSL Labs grade on which to warn" echo " --clientpass phrase Set passphrase for client certificate." echo " --configuration file Read options from the specified file" echo " --crl Check revocation via CRL (requires" echo " --rootcert-file)" echo " --curl-bin path Path of the curl binary to be used" echo " --custom-http-header string Custom HTTP header sent when getting the" echo " cert example: 'X-Check-Ssl-Cert: Foobar=1'" # Delimiter at 78 chars ############################################################ echo " --default-format Print the default output format and exit" echo " --dane Verify that valid DANE records exist" echo " (since OpenSSL 1.1.0)" echo " --dane 211 Verify that a valid DANE-TA(2) SPKI(1)" echo " SHA2-256(1) TLSA record exists" echo " --dane 301 Verify that a valid DANE-EE(3) Cert(0)" echo " SHA2-256(1) TLSA record exists" echo " --dane 302 Verify that a valid DANE-EE(3) Cert(0)" echo " SHA2-512(2) TLSA record exists" echo " --dane 311 Verify that a valid DANE-EE(3) SPKI(1)" echo " SHA2-256(1) TLSA record exists" echo " --dane 312 Verify that a valid DANE-EE(3)" echo " SPKI(1) SHA2-512(1) TLSA record exists" echo " --date path Path of the date binary to be used" echo " -d,--debug Produce debugging output (can be" echo " specified more than once)" echo " --debug-cert Store the retrieved certificates in the" echo " current directory" echo " --debug-headers Store the retrieved HTLM headers in the" echo " headers.txt file" echo " --debug-file file Write the debug messages to file" echo " --debug-time Write timing information in the" echo " debugging output" echo " --dig-bin path Path of the dig binary to be used" echo " --do-not-resolve Do not check if the host can be resolved" echo " --dtls Use the DTLS protocol" echo " --dtls1 Use the DTLS protocol 1.0" echo " --dtls1_2 Use the DTLS protocol 1.2" # Delimiter at 78 chars ############################################################ echo " -e,--email address Pattern to match the email address" echo " contained in the certificate" echo " --ecdsa Signature algorithm selection: force ECDSA" echo " certificate" echo " --element number Check up to the N cert element from the" echo " beginning of the chain" # Delimiter at 78 chars ############################################################ echo " --file-bin path Path of the file binary to be used" echo " --fingerprint hash Pattern to match the fingerprint" echo " --fingerprint-alg algorithm Algorithm for fingerprint. Default sha1" echo " --first-element-only Verify just the first cert element, not" echo " the whole chain" echo " --force-dconv-date Force the usage of dconv for date" echo " computations" echo " --force-perl-date Force the usage of Perl for date" echo " computations" echo " --format FORMAT Format output template on success, for" echo " example: '%SHORTNAME% OK %CN% from" echo " %CA_ISSUER_MATCHED%'" echo " list of possible variables:" echo " - %CA_ISSUER_MATCHED%" echo " - %CHECKEDNAMES%" echo " - %CN%" echo " - %DATE%" echo " - %DAYS_VALID%" echo " - %DYSPLAY_CN%" echo " - %HOST%" echo " - %OCSP_EXPIRES_IN_HOURS%" echo " - %OPENSSL_COMMAND%" echo " - %PORT%" echo " - %SELFSIGNEDCERT%" echo " - %SHORTNAME%" echo " - %SIGALGO%" echo " - %SSL_LABS_HOST_GRADE%" echo " See --default-format for the default" # Delimiter at 78 chars ############################################################ echo " --grep-bin path Path of the grep binary to be used" # Delimiter at 78 chars ############################################################ echo " -h,--help,-? This help message" echo " --http-headers-path path The path to be used to fetch HTTP headers" echo " --http-use-get Use GET instead of HEAD (default) for the" echo " HTTP related checks" # Delimiter at 78 chars ############################################################ echo " -i,--issuer issuer Pattern to match the issuer of the" echo " certificate" echo " --ignore-altnames Ignore alternative names when matching" echo " pattern specified in -n (or the host name)" echo " --ignore-connection-problems [state] In case of connection problems" echo " returns OK or the optional state" echo " --ignore-exp Ignore expiration date" echo " --ignore-http-headers Ignore checks on HTTP headers with --all" echo " and --all-local" echo " --ignore-host-cn Do not complain if the CN does not match" echo " the host name" echo " --ignore-incomplete-chain Do not check chain integrity" echo " --ignore-maximum-validity Ignore the certificate maximum validity" echo " --ignore-ocsp Do not check revocation with OCSP" echo " --ignore-ocsp-errors Continue if the OCSP status cannot be" echo " checked" echo " --ignore-ocsp-timeout Ignore OCSP result when timeout occurs" echo " while checking" echo " --ignore-sct Do not check for signed certificate" echo " timestamps (SCT)" echo " --ignore-sig-alg Do not check if the certificate was signed" echo " with SHA1 or MD5" echo " --ignore-ssl-labs-cache Force a new check by SSL Labs (see -L)" echo " --ignore-ssl-labs-errors Ignore errors if SSL Labs is not" echo " accessible or times out" echo " --ignore-tls-renegotiation Ignore the TLS renegotiation check" echo " --ignore-unexpected-eof Ignore unclean TLS shutdowns" echo " --inetproto protocol Force IP version 4 or 6" echo " --info Print certificate information" echo " --init-host-cache Initialize the host cache" echo " --issuer-cert-cache dir Directory where to store issuer" echo " certificates cache" # Delimiter at 78 chars ############################################################ echo " --jks-alias alias Alias name of the Java KeyStore entry" echo " (requires --file)" # Delimiter at 78 chars ############################################################ echo " -K,--clientkey path Use client certificate key to authenticate" # Delimiter at 78 chars ############################################################ echo " -L,--check-ssl-labs grade SSL Labs assessment (please check " echo " https://www.ssllabs.com/about/terms.html)" echo " --long-output list Append the specified comma separated (no" echo " spaces) list of attributes to the plugin" echo " output on additional lines" echo " Valid attributes are:" echo " enddate, startdate, subject, issuer," echo " modulus, serial, hash, email, ocsp_uri" echo " and fingerprint." echo " 'all' will include all the available" echo " attributes." # Delimiter at 78 chars ############################################################ echo " -m,--match Pattern to match the CN or AltName" echo " (can be specified multiple times)" echo " --maximum-validity [days] The maximum validity of the certificate" echo " must not exceed 'days' (default 397)" echo " This check is automatic for HTTPS" # Delimiter at 78 chars ############################################################ echo " --nmap-bin path Path of the nmap binary to be used" echo " --nmap-with-proxy Allow nmap to be used with a proxy" echo " --no-perf Do not show performance data" echo " --no-proxy Ignore the http_proxy and https_proxy" echo " environment variables" echo " --no-proxy-curl Ignore the http_proxy and https_proxy" echo " environment variables for curl" echo " --no-proxy-s_client Ignore the http_proxy and https_proxy" echo " environment variables for openssl s_client" echo " --no-ssl2 Disable SSL version 2" echo " --no-ssl3 Disable SSL version 3" echo " --no-tls1 Disable TLS version 1" echo " --no-tls1_1 Disable TLS version 1.1" echo " --no-tls1_2 Disable TLS version 1.2" echo " --no-tls1_3 Disable TLS version 1.3" echo " --not-issued-by issuer Check that the issuer of the certificate" echo " does not match the given pattern" echo " --not-valid-longer-than days Critical if the certificate validity is" echo " longer than the specified period" # Delimiter at 78 chars ############################################################ echo " -o,--org org Pattern to match the organization of the" echo " certificate" echo " --ocsp-critical hours Minimum number of hours an OCSP response" echo " has to be valid to issue a critical status" echo " --ocsp-warning hours Minimum number of hours an OCSP response" echo " has to be valid to issue a warning status" echo " --openssl path Path of the openssl binary to be used" # Delimiter at 78 chars ############################################################ echo " --path path Set the PATH variable to 'path'" echo " -p,--port port TCP port (default 443)" echo " --precision digits Number of decimal places for durations:" echo " defaults to 0 if critical or warning are" echo " integers, 2 otherwise" echo " -P,--protocol protocol Use the specific protocol:" echo " dns, ftp, ftps, http, https (default)," echo " h2 (HTTP/2), h3 (HTTP/3), imap, imaps," echo " irc, ircs, ldap, ldaps, mqtts, mysql," echo " pop3, pop3s, postgres, sieve, sips, smtp," echo " smtps, tds, xmpp, xmpp-server." echo " ftp, imap, irc, ldap, pop3, postgres," echo " sieve, smtp: switch to TLS using StartTLS" echo " --password source Password source for a local certificate," echo " see the PASS PHRASE ARGUMENTS section" echo " openssl(1)" echo " --prometheus Generate Prometheus/OpenMetrics output" echo " --proxy proxy Set http_proxy and the s_client -proxy" echo " option" echo " --python-bin path Path of the python binary to be used" # Delimiter at 78 chars ############################################################ echo " --quic Use QUIC" echo " -q,--quiet Do not produce any output" # Delimiter at 78 chars ############################################################ echo " -r,--rootcert path Root certificate or directory to be used" echo " for certificate validation" echo " --require-client-cert [list] The server must accept a client" echo " certificate. 'list' is an optional comma" echo " separated list of expected client" echo " certificate CAs" echo " --require-dnssec Require DNSSEC" echo " --require-http-header header Require the specified HTTP header" echo " (e.g., X-Frame-Options)" echo " --require-no-http-header header Require the absence of the specified" echo " HTTP header (e.g., X-Powered-By)" echo " --require-no-ssl2 Critical if SSL version 2 is offered" echo " --require-no-ssl3 Critical if SSL version 3 is offered" echo " --require-no-tls1 Critical if TLS 1 is offered" echo " --require-no-tls1_1 Critical if TLS 1.1 is offered" echo " --require-ocsp-stapling Require OCSP stapling" echo " --require-purpose usage Require the specified key usage (can be" echo " specified more then once)" echo " --require-purpose-critical The key usage must be critical" echo " --resolve-over-http [server] Resolve the host over HTTP using Google or" echo " the specified server" echo " --resolve ip Provide a custom IP address for the" echo " specified host" echo " --rootcert-dir path Root directory to be used for certificate" echo " validation" echo " --rootcert-file path Root certificate to be used for" echo " certificate validation" echo " --rsa Signature algorithm selection: force RSA" echo " certificate" # Delimiter at 78 chars ############################################################ echo " --security-level number Set the security level to specified value" echo " See SSL_CTX_set_security_level(3) for a" echo " description of what each level means" echo " -s,--selfsigned Allow self-signed certificates" echo " --serial serialnum Pattern to match the serial number" echo " --skip-element number Skip checks on the Nth cert element (can" echo " be specified multiple times)" echo " --sni name Set the TLS SNI (Server Name Indication)" echo " extension in the ClientHello message to" echo " 'name'" echo " --ssl2 Force SSL version 2" echo " --ssl3 Force SSL version 3" # Delimiter at 78 chars ############################################################ echo " -t,--timeout seconds Timeout after the specified time" echo " (defaults to ${TIMEOUT} seconds)" echo " --temp dir Directory where to store the temporary" echo " files" echo " --terse Terse output" echo " --tls1 Force TLS version 1" echo " --tls1_1 Force TLS version 1.1" echo " --tls1_2 Force TLS version 1.2" echo " --tls1_3 Force TLS version 1.3" # Delimiter at 78 chars ############################################################ echo " -u,--url URL HTTP request URL" echo " --user-agent string User agent that shall be used for HTTPS" echo " connections" # Delimiter at 78 chars ############################################################ echo " -v,--verbose Verbose output (can be specified more than" echo " once)" echo " -V,--version Version" # Delimiter at 78 chars ############################################################ echo " -w,--warning days Minimum number of days a certificate has" echo " to be valid to issue a warning status." echo " Can be a floating point number, e.g., 0.5" echo " Default: ${WARNING_DAYS}" # Delimiter at 78 chars ############################################################ echo " --xmpphost name Specify the host for the 'to' attribute" echo " of the stream element" # Delimiter at 78 chars ############################################################ echo " -4 Force IPv4" echo " -6 Force IPv6" echo echo "Deprecated options:" echo " --altnames Match the pattern specified in -n with" echo " alternate names too (enabled by default)" echo " -n,--cn name Pattern to match the CN or AltName" echo " (can be specified multiple times)" echo " --curl-user-agent string User agent that curl shall use to obtain" echo " the issuer cert" echo " --days days Minimum number of days a certificate has" echo " to be valid" echo " (see --critical and --warning)" echo " -N,--host-cn Match CN with the host name" echo " (enabled by default)" echo " --no_ssl2 Disable SSLv2 (deprecated use --no-ssl2)" echo " --no_ssl3 Disable SSLv3 (deprecated use --no-ssl3)" echo " --no_tls1 Disable TLSv1 (deprecated use --no-tls1)" echo " --no_tls1_1 Disable TLSv1.1 (deprecated use" echo " --no-tls1_1)" echo " --no_tls1_2 Disable TLSv1.1 (deprecated use" echo " --no-tls1_2)" echo " --no_tls1_3 Disable TLSv1.1 (deprecated use" echo " --no-tls1_3)" echo " --ocsp Check revocation via OCSP (enabled by" echo " default)" echo " --require-hsts Require HTTP Strict Transport Security" echo " (deprecated use --require-security-header" echo " strict-transport-security)" echo " --require-san Require the presence of a Subject" echo " Alternative Name" echo " extension" echo " --require-security-header header require the specified HTTP" echo " security header (e.g., X-Frame-Options)" echo " (deprecated use --require-http-header)" echo " --require-security-headers Require all the HTTP security headers:" echo " Content-Security-Policy" echo " Permissions-Policy" echo " Referrer-Policy" echo " strict-transport-security" echo " X-Content-Type-Options" echo " X-Frame-Options" echo " --require-security-headers-path path the path to be used to fetch HTTP" echo " security headers" echo " --require-x-frame-options [path] Require the presence of the" echo " X-Frame-Options HTTP header" echo " 'path' is the optional path to be used" echo " in the URL to check for the header" echo " (deprecated use --require-security-header" echo " X-Frame-Options and" echo " --require-security-headers-path path)" echo " -S,--ssl version Force SSL version (2,3)" echo " (see: --ssl2 or --ssl3)" echo echo "Report bugs to https://github.com/matteocorti/check_ssl_cert/issues" echo exit "${STATUS_UNKNOWN}" } ################################################################################ # Prints the given message to STDERR with the prefix '[DBG] ' if the debug # command line option was specified # # We are writing to STDERR since according to POSIX, STDERR is 'for writing diagnostic output' # see https://unix.stackexchange.com/questions/331611/do-progress-reports-logging-information-belong-on-stderr-or-stdout # # $1: string # $2: level (optional default 1) debuglog() { MESSAGE=$1 LEVEL=$2 if [ -n "${DEBUG_TIME}" ]; then NOW=$(date +%s) ELAPSED=$((NOW - DEBUG_TIME)) ELAPSED=$(printf "%04d" "${ELAPSED}") fi if [ -z "${LEVEL}" ]; then #default LEVEL=1 fi if [ "${LEVEL}" -le "${DEBUG}" ]; then if [ -n "${ELAPSED}" ]; then echo "${1}" | sed "s/^/[DBG ${ELAPSED}s] /" >&2 else echo "${1}" | sed "s/^/[DBG] /" >&2 fi fi # debuglog is also called during the --debug-file sanity checks: we have # to check if the file exists if [ -n "${DEBUG_FILE}" ] && [ -e "${DEBUG_FILE}" ] && ! [ -d "${DEBUG_FILE}" ] && [ -w "${DEBUG_FILE}" ]; then if [ -n "${DEBUG_TIME}" ]; then echo "+${ELAPSED}s ${1}" >>"${DEBUG_FILE}" else echo "${1}" >>"${DEBUG_FILE}" fi fi } ############################################################################## # Prints nicely certificate information info() { LABEL=$1 VALUE=$2 if [ -n "${INFO}" ] && [ -n "${VALUE}" ]; then if [ -n "${INFO_OUTPUT}" ] ; then INFO_OUTPUT="${INFO_OUTPUT}$( printf '\n%s\t%s\n' "${LABEL}" "${VALUE}" | expand -t 32 )" else INFO_OUTPUT="$( printf '%s\t%s\n' "${LABEL}" "${VALUE}" | expand -t 32 )" fi elif [ -n "${INFO}" ]; then if [ -n "${INFO_OUTPUT}" ] ; then INFO_OUTPUT="${INFO_OUTPUT}$( printf '\n%s\n' "${LABEL}" )" else INFO_OUTPUT="$( printf '%s\n' "${LABEL}" )" fi fi } ################################################################################ # Checks if the given file can be created and written # $1: file name open_for_appending() { FILE_TO_OPEN=$1 if [ -d "${FILE_TO_OPEN}" ]; then unknown "${FILE_TO_OPEN} is a directory" elif [ -e "${FILE_TO_OPEN}" ]; then # file already exists if [ ! -w "${FILE_TO_OPEN}" ]; then unknown "Cannot write to ${FILE_TO_OPEN}" fi else FILE_TO_OPEN_DIRECTORY=$(dirname "${FILE_TO_OPEN}") if [ ! -w "${FILE_TO_OPEN_DIRECTORY}" ]; then unknown "Cannot write to ${FILE_TO_OPEN}" fi # clear / create the file true >"${FILE_TO_OPEN}" fi } ################################################################################ # Checks if the given file can be created and written # $1: file name open_for_writing() { FILE_TO_OPEN=$1 if [ -d "${FILE_TO_OPEN}" ]; then unknown "${FILE_TO_OPEN} is a directory" elif [ -e "${FILE_TO_OPEN}" ]; then # file already exists if [ ! -w "${FILE_TO_OPEN}" ]; then unknown "Cannot write to ${FILE_TO_OPEN}" fi else FILE_TO_OPEN_DIRECTORY=$(dirname "${FILE_TO_OPEN}") if [ ! -w "${FILE_TO_OPEN_DIRECTORY}" ]; then unknown "Cannot write to ${FILE_TO_OPEN}" fi fi # clear / create the file true >"${FILE_TO_OPEN}" } ################################################################################ # Prints a warning on STDOUT about deprecated command line options # $1: command line option # $2: comment deprecated() { OPTION="$1" COMMENT="$2" echo "Command line option '${OPTION}' is deprecated: ${COMMENT}" >&2 } ################################################################################ # Prints the given message to STDOUT if the verbose command line option was # specified # $1: string # $2: level (optional default 1) verboselog() { MESSAGE=$1 LEVEL=$2 if [ -z "${LEVEL}" ]; then #default LEVEL=1 fi if [ "${LEVEL}" -le "${VERBOSE}" ]; then echo "${MESSAGE}" fi } ################################################################################ # trap passing the signal name # see https://stackoverflow.com/questions/2175647/is-it-possible-to-detect-which-trap-signal-in-bash/2175751#2175751 trap_with_arg() { func="$1" shift for sig; do # shellcheck disable=SC2064 trap "${func} ${sig}" "${sig}" done } ################################################################################ # Cleanup temporary files remove_temporary_files() { debuglog "cleaning up temporary files" # shellcheck disable=SC2086 if [ -n "${TEMPORARY_FILES}" ]; then TEMPORARY_FILES_TMP="$(echo "${TEMPORARY_FILES}" | tr '\s' '\n')" debuglog "${TEMPORARY_FILES_TMP}" rm -f ${TEMPORARY_FILES} fi } ################################################################################ # Cleanup when exiting cleanup() { SIGNAL=$1 debuglog "signal caught ${SIGNAL}" remove_temporary_files # shellcheck disable=SC2086 trap - ${SIGNALS} exit } create_temporary_file() { # create a temporary file # mktemp is not always available (e.g., on AIX) # we could use https://stackoverflow.com/questions/10224921/how-to-create-a-temporary-file-with-portable-shell-in-a-secure-way # but on some systems od -N4 -tu /dev/random takes seconds (?) to execute if [ -n "${MKTEMP}" ]; then TEMPFILE="$(mktemp "${TMPDIR}/XXXXXX" 2>/dev/null)" else TEMPFILE=${TMPDIR}/XXX-$(od -N4 -tu /dev/random | head -n 1 | sed 's/ *$//' | sed 's/.* //') touch "${TEMPFILE}" fi if [ -z "${TEMPFILE}" ] || [ ! -w "${TEMPFILE}" ]; then unknown 'temporary file creation failure.' fi debuglog "temporary file ${TEMPFILE} created" # add the file to the list of temporary files TEMPORARY_FILES="${TEMPORARY_FILES} ${TEMPFILE}" } ################################################################################ # Compute the number of hours until a given date # Params # $1 date # sets HOURS_UNTIL hours_until() { HU_DATE=$1 debuglog "Date computations: ${DATETYPE}" # we check if we are on a 32 bit system and if the date is beyond the max date # we simplify and consider a date invalid after 1.1.2038 instead of 19.1.2038 # since date is not able to parse the date we do it manually with a little bit of # heuristics ... LONG_BIT_TMP="$(getconf LONG_BIT 2> /dev/null)" if [ -z "${LONG_BIT_TMP}" ] ; then debuglog "Cannot detect system architecture: no LONGBIT" else if [ "${LONG_BIT_TMP}" -eq 32 ]; then debuglog "32 bit system" CERT_YEAR=$(echo "${HU_DATE}" | sed 's/.* \(2[0-9][0-9][0-9]\).*/\1/') debuglog "Checking if the year ${CERT_YEAR} is beyond the max date for the system 2038-01-19" if [ "${CERT_YEAR}" -gt 2038 ]; then verboselog "${HU_DATE} is beyond the maximum date on a 32 bit system: we consider 2038-01-19" HU_DATE='Jan 19 00:00:00 2038 GMT' fi fi fi debuglog "Computing number of hours until '${HU_DATE}' with ${DATETYPE}" case "${DATETYPE}" in "BSD") # new BSD date target_date=$(${DATEBIN} -jf "%b %d %T %Y %Z" "${HU_DATE}" +%s) now=$(${DATEBIN} +%s) HOURS_UNTIL=$(compute "(${target_date}-${now})/3600") ;; "DCONV") debuglog "Computing date with dconv" # detect the date -j required format if date --help 2>&1 | "${GREP_BIN}" -q '\[\[\[mm\]dd]HH\]MM\[\[cc\]yy\]\[\.ss\]\]'; then # e.g., macOS debuglog "date -j format [[[mm]dd]HH]MM[[cc]yy][.ss]]" debuglog "executing: echo '${HU_DATE}' | sed 's/ / /g' | ${DCONV_BIN} -f \"%m%d%H%M%Y.%S\" -i \"%b %d %H:%M:%S %Y %Z\"" CONVERTED_DATE=$(echo "${HU_DATE}" | sed 's/ / /g' | ${DCONV_BIN} -f "%m%d%H%M%Y.%S" -i "%b %d %H:%M:%S %Y %Z") debuglog "date converted with dconv: ${CONVERTED_DATE}" target_date=$(${DATEBIN} -j "${CONVERTED_DATE}" +%s) now=$(${DATEBIN} +%s) HOURS_UNTIL=$(compute "(${target_date}-${now})/3600") debuglog "hours computed with ${DCONV_BIN}: ${HOURS_UNTIL}" elif date --help 2>&1 | "${GREP_BIN}" -q '\[\[\[\[\[\[cc\]yy\]mm\]dd\]HH\]MM\[\.SS\]\]'; then # e.g., old BSD debuglog "date -j format [[[[[[cc]yy]mm]dd]HH]MM[.SS]]" CONVERTED_DATE=$(echo "${HU_DATE}" | sed 's/ / /g' | ${DCONV_BIN} -f "%Y%m%d%H%M.%S" -i "%b %d %H:%M:%S %Y %Z") debuglog "date converted with ${DCONV_BIN}: ${CONVERTED_DATE}" target_date=$(${DATEBIN} -j +%s "${CONVERTED_DATE}") now=$(${DATEBIN} +%s) HOURS_UNTIL=$(compute "(${target_date}-${now})/3600") else unknown "Unknown date(1) input format" fi ;; "BUSYBOX") BUSYBOX_DATE=$(echo "${HU_DATE}" | sed 's/[ ][^ ]*$//') debuglog "Computing number of hours until '${BUSYBOX_DATE}' (BusyBox compatible format)" target_date=$(${DATEBIN} -d "${BUSYBOX_DATE}" +%s) now=$(${DATEBIN} +%s) HOURS_UNTIL=$(compute "(${target_date}-${now})/3600") ;; "GNU") target_date=$(${DATEBIN} -d "${HU_DATE}" +%s) now=$(${DATEBIN} +%s) HOURS_UNTIL=$(compute "(${target_date}-${now})/3600") ;; "PERL") if ! perl -MDate::Parse -e '1;' > /dev/null 2>&1 ; then unknown "Error computing the certificate validity with Perl: Date::Parse not installed" fi # Warning: some shell script formatting tools will indent the EOF! (should be at position 0) if ! HOURS_UNTIL=$( perl - "${HU_DATE}" <<-"EOF" use strict; use warnings; use Date::Parse; my $cert_date = str2time( $ARGV[0] ); my $hours = int (( $cert_date - time ) / 3600 + 0.5); print "$hours\n"; EOF ); then # something went wrong with the embedded Perl code: check the indentation of EOF unknown "Error computing the certificate validity with Perl" fi ;; *) unknown "Internal error: unknown date type" ;; esac debuglog "Hours until ${HU_DATE}: ${HOURS_UNTIL}" } ################################################################################ # Convert a number of days into according number of seconds # Params # $1 NUMBER_OF_DAYS # return NUMBER_OF_SECONDS days_to_seconds() { NUMBER_OF_DAYS=$1 if echo "${NUMBER_OF_DAYS}" | "${GREP_BIN}" -q '^[0-9][0-9]*$'; then debuglog "Converting ${NUMBER_OF_DAYS} days into seconds by shell function" NUMBER_OF_SECONDS=$((NUMBER_OF_DAYS * 86400)) else if command -v perl >/dev/null; then debuglog "Converting ${NUMBER_OF_DAYS} days into seconds with perl" NUMBER_OF_SECONDS=$(perl -E "say ${NUMBER_OF_DAYS}*86400") else debuglog "Converting ${NUMBER_OF_DAYS} days into seconds with awk" NUMBER_OF_SECONDS=$(awk "BEGIN {print ${NUMBER_OF_DAYS} * 86400}") fi fi debuglog "Converted ${NUMBER_OF_DAYS} days into seconds: ${NUMBER_OF_SECONDS}" echo "${NUMBER_OF_SECONDS}" } ################################################################################ # checks if OpenSSL version is at least the given parameter # Params # $1 minimum version openssl_version() { # See https://wiki.openssl.org/index.php/Versioning # Required version MIN_VERSION=$1 debuglog "openssl_version ${MIN_VERSION}" IFS='.' read -r MIN_MAJOR1 MIN_MAJOR2 MIN_MINOR <&1) echo "${result}" } ############################################################################## # Check if the parameter is an integer # Params # $1 the value to check # $2 error message check_integer() { value=$1 message=$2 if ! echo "$1" | "${GREP_BIN}" -q '^[0-9][0-9]*$'; then unknown "${message}" fi } ############################################################################## # Check if the parameter is a float # Params # $1 the value to check # $2 error message check_float() { value=$1 message=$2 if ! echo "$1" | "${GREP_BIN}" -q -E '^[0-9][0-9]*([.][0-9][0-9]*)?$'; then unknown "${message}" fi } ################################################################################ # Exits with unknown if s_client does not support the given option # # Usage: # require_s_client_option '-no_ssl2' # require_s_client_option() { debuglog "Checking if s_client supports the $1 option" if ! "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -- "$1"; then unknown "s_client does not support the $1 option" fi } ################################################################################ # Exits with unknown if x509 does not support the given option # # Usage: # require_x509_option '-no_ssl2' # require_x509_option() { debuglog "Checking if x509 supports the required $1 option" if ! "${OPENSSL}" x509 -help 2>&1 | "${GREP_BIN}" -q -- "[[:blank:]]$1[[:blank:]]"; then unknown "x509 does not support the $1 option$2" fi } ################################################################################ # Exits with unknown if x509 does not support the given option # $1 option to be checked # returns 0 for true and 1 for false # # Usage: # check_x509_option '-no_ssl2' # check_x509_option() { debuglog "Checking if x509 supports the $1 option" "${OPENSSL}" x509 -help 2>&1 | "${GREP_BIN}" -q -- "[[:blank:]]$1[[:blank:]]" return $? } ################################################################################ # Extract specific field from a subject # $1 field # $2 subject parse_subject() { FIELD=$1 SUBJECT=$2 debuglog "Extracting '${FIELD}' from '${SUBJECT}'" # on older systems the fields where separated by /, e.g., # subject= /C=US/ST=California/L=San Francisco/O=GitHub, Inc./CN=github.com # on newer systems the fields are separated by , and are sometimes quoted (if they contain a comma), e.g., # subject=C = US, ST = California, L = San Francisco, O = "GitHub, Inc.", CN = github.com # subject=C = ES, ST = Madrid, L = Madrid, jurisdictionC = ES, O = Ibermutua Mutua Colaboradora con la Seguridad Social N\C3\BAmero 274, businessCategory = Private Organization, serialNumber = 1998-02-18, CN = www.ibermutua.es # if the field could contain UTF-8 characters (and -nameopt utf8 is specified) there it no space around = if echo "${SUBJECT}" | "${GREP_BIN}" -q '^subject=[ ][/]'; then # old format debuglog " old format separated by /" if echo "${SUBJECT}" | "${GREP_BIN}" -q "${FIELD}="; then echo "${SUBJECT}" | sed -e "s/.*\\/${FIELD}=//" -e 's/\/.*//' fi else # new format debuglog " new format separated by ," if echo "${SUBJECT}" | "${GREP_BIN}" -q "${FIELD}[ ]*="; then if echo "${SUBJECT}" | "${GREP_BIN}" -q "${FIELD}[ ]*=[ ]*\""; then # quotes debuglog " quotes" echo "${SUBJECT}" | sed -e "s/.*${FIELD} *= *\"//" -e 's/".*//' else # no quotes debuglog " no quotes" echo "${SUBJECT}" | sed -e "s/.*${FIELD} *= *//" -e 's/, [^,]*$//' fi fi fi } ################################################################################ # Extract specific attributes from a certificate # $1 attribute name # $2 cert file or cert content extract_cert_attribute() { debuglog "extracting cert attribute ${1}" if [ -f "${2}" ]; then cert_content="$(cat "${2}")" else cert_content="${2}" fi # shellcheck disable=SC2086,SC2016 case $1 in cn) if echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout ${OPENSSL_PARAMS} -subject 2>/dev/null | "${GREP_BIN}" -F -q 'CN' >/dev/null; then echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout ${OPENSSL_PARAMS} -subject | sed -e "s/^.*[[:space:]]*CN[[:space:]]=[[:space:]]//" -e 's/\/[[:alpha:]][[:alpha:]]*=.*$//' -e "s/,.*//" else echo 'CN unavailable' return 1 fi ;; subject) # the Subject could contain UTF-8 characters echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout ${OPENSSL_PARAMS} -subject -nameopt utf8 ;; serial) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -serial | sed -e "s/^serial=//" ;; fingerprint) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -fingerprint -${FINGERPRINT_ALG} | sed -e "s/^${FINGERPRINT_ALG} Fingerprint=//" ;; oscp_uri) echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -ocsp_uri ;; oscp_uri_single) extract_cert_attribute 'oscp_uri' "${cert_content}" | head -n 1 ;; hash) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -hash ;; modulus) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -modulus ;; issuer) # see https://unix.stackexchange.com/questions/676776/parse-comma-separated-string-ignoring-commas-between-quotes echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout -nameopt sep_multiline,utf8,esc_ctrl -issuer | tail -n +2 | sed 's/^ *//' ;; issuer_uri) echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -text | "${GREP_BIN}" -F "CA Issuers" | "${GREP_BIN}" -F -i "http" | sed -e "s/^.*CA Issuers - URI://" | tr -d '"!|;${}<>`&' ;; issuer_uri_single) extract_cert_attribute 'issuer_uri' "${cert_content}" | head -n 1 ;; issuer_hash) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -issuer_hash ;; org) cert_subject=$(echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -nameopt utf8 -noout -subject) parse_subject "O" "${cert_subject}" ;; org_unit) cert_subject=$(echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -nameopt utf8 -noout -subject) parse_subject "OU" "${cert_subject}" ;; key_length) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -text | "${GREP_BIN}" 'Public-Key:' | sed -e 's/.*(//' | sed -e 's/).*//' ;; country) cert_subject=$(echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -nameopt utf8 -noout -subject) parse_subject "C" "${cert_subject}" ;; state) cert_subject=$(echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -nameopt utf8 -noout -subject) parse_subject "ST" "${cert_subject}" ;; locality) cert_subject=$(echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -nameopt utf8 -noout -subject) parse_subject "L" "${cert_subject}" ;; email) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -email ;; crl_uri) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -text | "${GREP_BIN}" -A 4 'X509v3 CRL Distribution Points' | "${GREP_BIN}" 'URI:' | sed 's/.*URI://' | head -n 1 ;; version) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -text | "${GREP_BIN}" Version | head -n 1 | sed 's/.*Version: //' ;; pub_key_algo) # The Signature Algorithm refers to the signature of the certificate created by the issuer # The Public Key Algorithm refers to the public key inside the certificate # # see https://security.stackexchange.com/questions/141661/whats-the-difference-between-public-key-algorithm-and-signature-algorithm-i ALGORITHM=$(echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -text | "${GREP_BIN}" -m 1 -F 'Public Key Algorithm' | sed -e 's/.*: //') PUBLIC_KEY=$(echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -text | "${GREP_BIN}" -m 1 -F 'Signature' | sed 's/.*: //') echo "${ALGORITHM} ${PUBLIC_KEY}" ;; sig_algo) # The Signature Algorithm refers to the signature of the certificate created by the issuer # The Public Key Algorithm refers to the public key inside the certificate # # see https://security.stackexchange.com/questions/141661/whats-the-difference-between-public-key-algorithm-and-signature-algorithm-i ALGORITHM=$(echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -text | "${GREP_BIN}" -m 1 -F 'Signature Algorithm' | sed -e 's/.*: //') PUBLIC_KEY=$(echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -text | "${GREP_BIN}" -m 1 -F 'Public-Key' | sed 's/.*: //') echo "${ALGORITHM} ${PUBLIC_KEY}" ;; startdate) echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} -startdate | sed -e "s/^notBefore=//" ;; enddate) echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" -in /dev/stdin -noout ${OPENSSL_PARAMS} "${OPENSSL_ENDDATE_OPTION}" | sed -e "s/^notAfter=//" -e "s/^nextUpdate=//" ;; sct) echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -text | "${GREP_BIN}" -E -q 'SCTs|1\.3\.6\.1\.4\.1\.11129\.2\.4\.2' ;; subjectAlternativeName) echo "${cert_content}" | "${OPENSSL}" "${OPENSSL_COMMAND}" ${OPENSSL_PARAMS} -in /dev/stdin -text | "${GREP_BIN}" -F -A 1 "509v3 Subject Alternative Name:" | tail -n 1 | sed -e "s/DNS://g" | sed -e "s/IP Address://g" | sed -e "s/,//g" | sed -e 's/^ *//' ;; keyUsage) KEY_USAGE_TMP=$(echo "${cert_content}" | "${OPENSSL}" x509 -in /dev/stdin -noout -ext keyUsage 2>&1) if echo "${KEY_USAGE_TMP}" | "${GREP_BIN}" -q 'No extensions in certificate'; then echo else if echo "${KEY_USAGE_TMP}" | "${GREP_BIN}" -q critical; then PURPOSE_CRITICAL=1 fi PURPOSE=$(echo "${KEY_USAGE_TMP}" | tail -n 1 | sed 's/^[[:blank:]]*//') echo "${PURPOSE}" fi ;; *) return 1 ;; esac } ################################################################################ # Executes command with a timeout # Params: # $1 command # $2 where to put the stdout # $3 where to put the stderr # if ${TIMEOUT_REASON} is set, it is added to the error message # Returns 1 if timed out 0 otherwise exec_with_timeout() { NOW=$(date +%s) ELAPSED=$((NOW - START_TIME)) CURRENT_TIMEOUT=$(( TIMEOUT - ELAPSED)) debuglog "exec_with_timeout: TIMEOUT=${TIMEOUT}, CURRENT_TIMEOUT=${CURRENT_TIMEOUT}, ELAPSED=${ELAPSED}" if [ -n "${TIMEOUT_REASON}" ]; then if ! echo "${TIMEOUT_REASON}" | "${GREP_BIN}" -q '^ '; then # add a blank before the reason in parenthesis TIMEOUT_REASON=" (${TIMEOUT_REASON})" fi fi if [ "${CURRENT_TIMEOUT}" -lt 1 ]; then # the timeout is already reached (before executing the command) prepend_critical_message "Timeout after ${ELAPSED} seconds" critical "${SHORTNAME} CRITICAL: Timeout after ${ELAPSED} seconds${TIMEOUT_REASON}" fi # start the command in a subshell to avoid problem with pipes # (spawn accepts one command) command="/bin/sh -c \"$1\"" OUTFILE=/dev/null if [ -n "$2" ]; then OUTFILE=$2 fi ERRFILE=/dev/null if [ -n "$3" ]; then ERRFILE=$3 fi debuglog "exec_with_timeout $1 $2 $3" debuglog "executing with timeout (${CURRENT_TIMEOUT}s): $1" if [ -n "${TIMEOUT_BIN}" ]; then debuglog "$(printf '%s %s %s\n' "${TIMEOUT_BIN}" "${CURRENT_TIMEOUT}" "${command}")" # We execute timeout in the background so that it can be relay a signal to 'timeout' # https://unix.stackexchange.com/questions/57667/why-cant-i-kill-a-timeout-called-from-a-bash-script-with-a-keystroke/57692#57692 eval "${TIMEOUT_BIN} ${CURRENT_TIMEOUT} ${command} &" >"${OUTFILE}" 2>"${ERRFILE}" TIMEOUT_PID=$! wait "${TIMEOUT_PID}" >/dev/null 2>&1 RET=$? # return codes # https://www.gnu.org/software/coreutils/manual/coreutils.html#timeout-invocation # because of the execution in the background we get a 137 for a timeout if [ "${RET}" -eq 137 ] || [ "${RET}" -eq 124 ]; then prepend_critical_message "Timeout after ${ELAPSED} seconds" critical "${SHORTNAME} CRITICAL: Timeout after ${ELAPSED} seconds${TIMEOUT_REASON}" elif [ "${RET}" -eq 125 ]; then prepend_critical_message "execution of ${command} failed" elif [ "${RET}" -eq 126 ]; then prepend_critical_message "${command} is found but cannot be invoked" elif [ "${RET}" -eq 127 ]; then prepend_critical_message "${command} cannot be found" fi return "${RET}" elif [ -n "${EXPECT}" ]; then # just to tell shellcheck that the variable is assigned # (in fact the value is assigned with the function set_value) EXPECT_SCRIPT='' TIMEOUT_ERROR_CODE=42 set_variable EXPECT_SCRIPT < ${OUTFILE} 2> ${ERRFILE} } expect { timeout { exit ${TIMEOUT_ERROR_CODE} } eof } # Get the return value # https://stackoverflow.com/questions/23614039/how-to-get-the-exit-code-of-spawned-process-in-expect-shell-script foreach { pid spawnid os_error_flag value } [wait] break # return the command return value exit \$value EOT debuglog 'Executing expect script' debuglog "$(printf '%s' "${EXPECT_SCRIPT}")" echo "${EXPECT_SCRIPT}" | expect RET=$? debuglog "expect returned ${RET}" if [ "${RET}" -eq "${TIMEOUT_ERROR_CODE}" ]; then prepend_critical_message "Timeout after ${ELAPSED} seconds" critical "${SHORTNAME} CRITICAL: Timeout after ${ELAPSED} seconds${TIMEOUT_REASON}" fi return "${RET}" else debuglog "$(printf '%s\n' eval "${command}")" debuglog " output: ${OUTFILE}" debuglog " error: ${ERRFILE}" eval "${command}" >"${OUTFILE}" 2>"${ERRFILE}" RET=$? return "${RET}" fi } ################################################################################ # Checks if a given program is available and executable # Params # $1 program name # Returns 1 if the program exists and is executable check_required_prog() { PROG=$(command -v "$1" 2>/dev/null) if [ -z "${PROG}" ]; then unknown "cannot find program: $1" fi if [ ! -x "${PROG}" ]; then unknown "${PROG} is not executable" fi } ################################################################################ # Checks cert revocation via CRL # Params # $1 cert # $2 element number check_crl() { el_number=1 if [ -n "$2" ]; then el_number=$2 fi create_temporary_file CERT_ELEMENT=${TEMPFILE} debuglog "Storing the chain element in ${CERT_ELEMENT}" echo "${1}" >"${CERT_ELEMENT}" # We check all the elements of the chain (but the root) for revocation # If any element is revoked, the certificate should not be trusted # https://security.stackexchange.com/questions/5253/what-happens-when-an-intermediate-ca-is-revoked debuglog "Checking CRL status of element ${el_number}" # See https://raymii.org/s/articles/OpenSSL_manually_verify_a_certificate_against_a_CRL.html CRL_URI="$(extract_cert_attribute 'crl_uri' "${CERT_ELEMENT}")" if [ -n "${CRL_URI}" ]; then debuglog "Certificate revocation list available (${CRL_URI})." debuglog "CRL: fetching CRL ${CRL_URI} to ${CRL_TMP}" TIMEOUT_REASON="fetching CRL" if [ -n "${HTTP_USER_AGENT}" ]; then exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --user-agent '${HTTP_USER_AGENT}' --location \\\"${CRL_URI}\\\" > ${CRL_TMP}" else exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --location \\\"${CRL_URI}\\\" > ${CRL_TMP}" fi unset TIMEOUT_REASON if "${FILE_BIN}" -L -b "${CRL_TMP}" | "${GREP_BIN}" -E -q '(data|Certificate)'; then # convert DER to PEM debuglog "Converting ${CRL_TMP} (DER) to ${CRL_TMP_PEM} (PEM)" "${OPENSSL}" crl -inform DER -in "${CRL_TMP}" -outform PEM -out "${CRL_TMP_PEM}" else # file already in PEM format CRL_TMP_PEM="${CRL_TMP}" fi # combine the certificate and the CRL debuglog "Combining the certificate, the CRL and the root cert" debuglog "cat ${CRL_TMP_PEM} ${CERT} ${ROOT_CA_FILE} > ${CRL_TMP_CHAIN}" cat "${CRL_TMP_PEM}" "${CERT}" "${ROOT_CA_FILE}" >"${CRL_TMP_CHAIN}" debuglog "${OPENSSL} verify -crl_check -CRLfile ${CRL_TMP_PEM} ${CERT_ELEMENT}" CRL_RESULT=$( "${OPENSSL}" verify -crl_check -CAfile "${CRL_TMP_CHAIN}" -CRLfile "${CRL_TMP_PEM}" "${CERT_ELEMENT}" 2>&1 | "${GREP_BIN}" -F ':' | head -n 1 | sed 's/^.*: //' ) debuglog " result: ${CRL_RESULT}" if ! [ "${CRL_RESULT}" = 'OK' ]; then prepend_critical_message "certificate element ${el_number} is revoked (CRL)" fi else debuglog "Certificate revocation list not available" fi } ################################################################################ # Checks cert revocation via OCSP # Params # $1 cert # $2 element number check_ocsp() { el_number=1 if [ -n "$2" ]; then el_number=$2 fi # We check all the elements of the chain (but the root) for revocation # If any element is revoked, the certificate should not be trusted # https://security.stackexchange.com/questions/5253/what-happens-when-an-intermediate-ca-is-revoked debuglog "Checking OCSP status of element ${el_number}" create_temporary_file CERT_ELEMENT=${TEMPFILE} debuglog "Storing the chain element in ${CERT_ELEMENT}" echo "${1}" >"${CERT_ELEMENT}" ################################################################################ # Check revocation via OCSP if [ -n "${OCSP}" ]; then debuglog "Checking revocation via OCSP" ISSUER_HASH="$(extract_cert_attribute 'issuer_hash' "${CERT_ELEMENT}")" debuglog "Issuer hash: ${ISSUER_HASH}" if [ -z "${ISSUER_HASH}" ]; then critical 'unable to find issuer certificate hash.' fi ISSUER_CERT= if [ -n "${ISSUER_CERT_CACHE}" ]; then if [ -r "${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" ]; then debuglog "Found cached Issuer Certificate: ${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" ISSUER_CERT="${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" else debuglog "Not found cached Issuer Certificate: ${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" fi fi ELEMENT_ISSUER_URIS="$(extract_cert_attribute 'issuer_uri' "${CERT_ELEMENT}")" if [ -z "${ELEMENT_ISSUER_URIS}" ]; then verboselog "Warning cannot find the CA Issuers in the certificate chain element ${el_number}: disabling OCSP checks on chain element ${el_number}" return fi debuglog "Chain element issuer URIs: ${ELEMENT_ISSUER_URIS}" for ELEMENT_ISSUER_URI in ${ELEMENT_ISSUER_URIS}; do debuglog "checking issuer URIs: ${ELEMENT_ISSUER_URI}" # shellcheck disable=SC2021 ELEMENT_ISSUER_URI_WO_SPACES_TMP="$(echo "${ELEMENT_ISSUER_URI}" | tr -d '[[:space:]]')" if [ "${ELEMENT_ISSUER_URI}" != "${ELEMENT_ISSUER_URI_WO_SPACES_TMP}" ]; then verboselog "Warning: unable to fetch the CA issuer certificate (spaces in URI): skipping" continue elif ! echo "${ELEMENT_ISSUER_URI}" | "${GREP_BIN}" -q -i '^http'; then verboselog "Warning: unable to fetch the CA issuer certificate (unsupported protocol): skipping" continue fi if [ -z "${ISSUER_CERT}" ]; then debuglog "OCSP: fetching issuer certificate ${ELEMENT_ISSUER_URI} to ${ISSUER_CERT_TMP}" TIMEOUT_REASON="OCSP: fetching issuer ${ELEMENT_ISSUER_URI}" if [ -n "${HTTP_USER_AGENT}" ]; then exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --user-agent '${HTTP_USER_AGENT}' --location \\\"${ELEMENT_ISSUER_URI}\\\" > ${ISSUER_CERT_TMP}" else exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --location \\\"${ELEMENT_ISSUER_URI}\\\" > ${ISSUER_CERT_TMP}" fi unset TIMEOUT_REASON TYPE_TMP="$(${FILE_BIN} -L -b "${ISSUER_CERT_TMP}" | sed 's/.*://')" debuglog "OCSP: issuer certificate type (1): ${TYPE_TMP}" if echo "${ELEMENT_ISSUER_URI}" | "${GREP_BIN}" -F -q 'p7c'; then debuglog "OCSP: converting issuer certificate from PKCS #7 to PEM" open_for_writing "${ISSUER_CERT_TMP2}" cp "${ISSUER_CERT_TMP}" "${ISSUER_CERT_TMP2}" ${OPENSSL} pkcs7 -print_certs -inform DER -outform PEM -in "${ISSUER_CERT_TMP2}" -out "${ISSUER_CERT_TMP}" fi TYPE_TMP="$(${FILE_BIN} -L -b "${ISSUER_CERT_TMP}" | sed 's/.*://')" debuglog "OCSP: issuer certificate type (2): ${TYPE_TMP}" # check for errors if "${FILE_BIN}" -L -b "${ISSUER_CERT_TMP}" | "${GREP_BIN}" -E -q HTML; then debuglog "OCSP: HTML page returned instead of a certificate" critical "Unable to fetch a valid certificate issuer certificate (HTML page returned)." fi # check the result if ! "${FILE_BIN}" -L -b "${ISSUER_CERT_TMP}" | "${GREP_BIN}" -E -q '(ASCII|PEM)'; then if "${FILE_BIN}" -L -b "${ISSUER_CERT_TMP}" | "${GREP_BIN}" -E -q '(data|Certificate)'; then debuglog "OCSP: converting issuer certificate from DER to PEM" open_for_writing "${ISSUER_CERT_TMP2}" cp "${ISSUER_CERT_TMP}" "${ISSUER_CERT_TMP2}" ${OPENSSL} x509 -in /dev/stdin -inform DER -outform PEM -in "${ISSUER_CERT_TMP2}" -out "${ISSUER_CERT_TMP}" elif "${FILE_BIN}" -L -b "${ISSUER_CERT_TMP}" | "${GREP_BIN}" -E -q 'empty'; then # empty certs are allowed debuglog "OCSP empty certificate detected: skipping" return else TYPE_TMP="$(${FILE_BIN} -L -b "${ISSUER_CERT_TMP}")" debuglog "OCSP: complete issuer certificate type ${TYPE_TMP}" critical "Unable to fetch a valid certificate issuer certificate." fi fi TYPE_TMP="$(${FILE_BIN} -L -b "${ISSUER_CERT_TMP}" | sed 's/.*://')" debuglog "OCSP: issuer certificate type (3): ${TYPE_TMP}" if [ -n "${DEBUG_CERT}" ]; then # remove trailing / FILE_NAME=${ELEMENT_ISSUER_URI%/} # remove everything up to the last slash FILE_NAME="${FILE_NAME##*/}" debuglog "OCSP: storing a copy of the retrieved issuer certificate to ${FILE_NAME} for debugging purposes" open_for_writing "${FILE_NAME}" cp "${ISSUER_CERT_TMP}" "${FILE_NAME}" fi if [ -n "${ISSUER_CERT_CACHE}" ]; then if [ ! -w "${ISSUER_CERT_CACHE}" ]; then unknown "Issuer certificates cache ${ISSUER_CERT_CACHE} is not writable!" fi debuglog "Storing Issuer Certificate to cache: ${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" open_for_writing "${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" cp "${ISSUER_CERT_TMP}" "${ISSUER_CERT_CACHE}/${ISSUER_HASH}.crt" fi ISSUER_CERT=${ISSUER_CERT_TMP} fi done OCSP_URIS="$(extract_cert_attribute 'oscp_uri' "${CERT_ELEMENT}")" debuglog "OCSP: URIs = ${OCSP_URIS}" for OCSP_URI in ${OCSP_URIS}; do debuglog "OCSP: URI = ${OCSP_URI}" OCSP_HOST="$(echo "${OCSP_URI}" | sed -e 's@.*//\([^/]\+\)\(/.*\)\?$@\1@g' | sed 's/^http:\/\///' | sed 's/\/.*//')" debuglog "OCSP: host = ${OCSP_HOST}" # ocsp has an own timeout option if [ -n "${OCSP_HOST}" ]; then # check if -header is supported OCSP_HEADER="" # ocsp -header is supported in OpenSSL versions from 1.0.0, but not documented until 1.1.0 # so we check if the major version is greater than 0 OPENSSL_VERSION_TMP="$(${OPENSSL} version | sed -e 's/OpenSSL \([0-9]\).*/\1/g')" if "${OPENSSL}" version | "${GREP_BIN}" -q '^LibreSSL' || [ "${OPENSSL_VERSION_TMP}" -gt 0 ]; then debuglog "openssl ocsp supports the -header option" # the -header option was first accepting key and value separated by space. The newer versions are using key=value KEYVALUE="" if ${OPENSSL} ocsp -help 2>&1 | "${GREP_BIN}" -F header | "${GREP_BIN}" -F -q 'key=value'; then debuglog "${OPENSSL} ocsp -header requires 'key=value'" KEYVALUE=1 else debuglog "${OPENSSL} ocsp -header requires 'key value'" fi # http_proxy is sometimes lower- and sometimes uppercase. Programs usually check both # shellcheck disable=SC2154 if [ -n "${http_proxy}" ]; then HTTP_PROXY="${http_proxy}" fi if [ -n "${HTTP_PROXY:-}" ]; then OCSP_PROXY_ARGUMENT="$(echo "${HTTP_PROXY}" | sed 's/.*:\/\///' | sed 's/\/$//')" debuglog "OCSP_PROXY_ARGUMENT = ${OCSP_PROXY_ARGUMENT}" OPENSSL_VERSION_SHORT=${OPENSSL_VERSION%%.*} debuglog "OpenSSL major version = ${OPENSSL_VERSION_SHORT}" if [ -n "${KEYVALUE}" ]; then if [ "${OPENSSL_VERSION_SHORT}" -ge 3 ];then debuglog "executing (1) ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT_ELEMENT} ${OCSP_HEADER} -header HOST=${OCSP_HOST} -proxy ${http_proxy} -url ${OCSP_URI}" OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -header "HOST=${OCSP_HOST}" -proxy "${http_proxy}" -url "${OCSP_URI}" 2>&1)" else debuglog "executing (2) ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT_ELEMENT} ${OCSP_HEADER} -header HOST=${OCSP_HOST} -host ${OCSP_PROXY_ARGUMENT} -path ${OCSP_URI}" OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -header "HOST=${OCSP_HOST}" -host "${OCSP_PROXY_ARGUMENT}" -path "${OCSP_URI}" 2>&1)" fi else if [ "${OPENSSL_VERSION_SHORT}" -ge 3 ];then debuglog "executing (3) ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT_ELEMENT} ${OCSP_HEADER} -header HOST ${OCSP_HOST} -host ${OCSP_PROXY_ARGUMENT} -path ${OCSP_URI}" OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -header HOST "${OCSP_HOST}" -host "${OCSP_PROXY_ARGUMENT}" -path "${OCSP_URI}" 2>&1)" else debuglog "executing (4) ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT_ELEMENT} ${OCSP_HEADER} -header HOST ${OCSP_HOST} -host ${OCSP_PROXY_ARGUMENT} -path ${OCSP_URI}" OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -header HOST "${OCSP_HOST}" -host "${OCSP_PROXY_ARGUMENT}" -path "${OCSP_URI}" 2>&1)" fi fi else if [ -n "${KEYVALUE}" ]; then debuglog "executing (5) ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT_ELEMENT} -url ${OCSP_URI} ${OCSP_HEADER} -header HOST=${OCSP_HOST}" OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -url "${OCSP_URI}" -header "HOST=${OCSP_HOST}" 2>&1)" else debuglog "executing (6) ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer ${ISSUER_CERT} -cert ${CERT_ELEMENT} -url ${OCSP_URI} ${OCSP_HEADER} -header HOST ${OCSP_HOST}" OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -url "${OCSP_URI}" -header HOST "${OCSP_HOST}" 2>&1)" fi fi MESSAGE_TMP="$(echo "${OCSP_RESP}" | sed 's/^/OCSP: response = /')" debuglog "${MESSAGE_TMP}" if [ -n "${OCSP_IGNORE_TIMEOUT}" ] && echo "${OCSP_RESP}" | "${GREP_BIN}" -F -q -i "timeout on connect"; then debuglog 'OCSP: Timeout on connect' elif echo "${OCSP_RESP}" | "${GREP_BIN}" -F -q -i "revoked"; then debuglog 'OCSP: revoked' prepend_critical_message "certificate element ${el_number} is revoked (OCSP)" elif echo "${OCSP_RESP}" | "${GREP_BIN}" -F -q -i "internalerror" && [ -n "${OCSP_IGNORE_ERRORS}" ]; then verboselog 'warning: the OCSP server returned an internal error' elif ! echo "${OCSP_RESP}" | "${GREP_BIN}" -F -q -i "good"; then debuglog "OCSP: not good. HTTP_PROXY = ${HTTP_PROXY}" if [ -n "${HTTP_PROXY:-}" ]; then debuglog "executing ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer \"${ISSUER_CERT}\" -cert \"${CERT_ELEMENT}]\" -host \"${HTTP_PROXY#*://}\" -path \"${OCSP_URI}\" \"${OCSP_HEADER}\" 2>&1" if [ -n "${OCSP_HEADER}" ]; then OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -host "${HTTP_PROXY#*://}" -path "${OCSP_URI}" "${OCSP_HEADER}" 2>&1)" else OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -host "${HTTP_PROXY#*://}" -path "${OCSP_URI}" 2>&1)" fi else debuglog "executing ${OPENSSL} ocsp -timeout \"${CURRENT_TIMEOUT}\" -no_nonce -issuer \"${ISSUER_CERT}\" -cert \"${CERT_ELEMENT}\" -url \"${OCSP_URI}\" \"${OCSP_HEADER}\" 2>&1" if [ -n "${OCSP_HEADER}" ]; then OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -url "${OCSP_URI}" "${OCSP_HEADER}" 2>&1)" else OCSP_RESP="$(${OPENSSL} ocsp -timeout "${CURRENT_TIMEOUT}" -no_nonce -issuer "${ISSUER_CERT}" -cert "${CERT_ELEMENT}" -url "${OCSP_URI}" 2>&1)" fi fi debuglog "${OCSP_RESP}" OCSP_ERROR_MESSAGE=$(echo "${OCSP_RESP}" | head -n 1) if [ -z "${OCSP_IGNORE_ERRORS}" ]; then prepend_critical_message "OCSP error (${OCSP_ERROR_MESSAGE})" else debuglog "Ignoring OCSP error (${OCSP_ERROR_MESSAGE})" fi fi else verboselog "Warning: openssl ocsp does not support the -header option: disabling OCSP checks" fi else verboselog "Warning: no OCSP host found: disabling OCSP checks" fi done verboselog "OCSP check for element ${el_number} OK" fi } ################################################################################ # Checks cert end date validity # Params # $1 cert # $2 element number # Returns number of days check_cert_end_date() { el_number=1 if [ -n "$2" ]; then el_number=$2 else debuglog "No certificate element specified: default 1" fi replace_current_message='' element_cn=$(extract_cert_attribute 'cn' "${1}") debuglog "Checking expiration date of element ${el_number} (${element_cn})" ELEM_END_DATE="$(extract_cert_attribute 'enddate' "$1")" debuglog "Validity date on cert element ${el_number} (${element_cn}) is ${ELEM_END_DATE}" hours_until "${ELEM_END_DATE}" debuglog "HOURS_UNTIL=${HOURS_UNTIL}" # TO DO: floating point ELEM_DAYS_VALID=$(compute "${HOURS_UNTIL}/24") ELEM_SECONDS_VALID=$(compute "${HOURS_UNTIL} * 3600") add_prometheus_days_output_line "cert_days_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} ${ELEM_DAYS_VALID}" debuglog " valid for ${ELEM_DAYS_VALID} days" if [ -z "${EARLIEST_VALIDITY_HOURS}" ] || compare "${HOURS_UNTIL}" "<" "${EARLIEST_VALIDITY_HOURS}"; then EARLIEST_VALIDITY_HOURS="${HOURS_UNTIL}" replace_current_message='yes' fi if [ -z "${DAYS_VALID}" ] || compare "${ELEM_DAYS_VALID}" "<" "${DAYS_VALID}"; then DAYS_VALID="${ELEM_DAYS_VALID}" fi add_performance_data "days_chain_elem${el_number}=${ELEM_DAYS_VALID};${WARNING_DAYS};${CRITICAL_DAYS};;" if [ "${OPENSSL_COMMAND}" = "x509" ]; then # x509 certificates (default) # We always check expired certificates debuglog "executing: ${OPENSSL} x509 -in /dev/stdin -noout -checkend 0 on cert element ${el_number} (${element_cn})" if ! echo "${1}" | ${OPENSSL} x509 -in /dev/stdin -noout -checkend 0 >/dev/null; then if compare "${ELEM_DAYS_VALID}" ">=" 0 && compare "${ELEM_DAYS_VALID}" "<" 1; then DAYS_AGO='less than a day ago' else # remove decimals ELEM_DAYS_VALID=$( echo "${ELEM_DAYS_VALID}" | sed -e 's/[.].*//' ) DAYS_AGO="$((-ELEM_DAYS_VALID)) days ago" fi debuglog "CRITICAL: certificate element ${el_number} (${element_cn}) is expired (was valid until ${ELEM_END_DATE}, ${DAYS_AGO})" CN_EXPIRED_TMP="${element_cn}:${replace_current_message}:${OPENSSL_COMMAND} certificate element ${el_number} (${element_cn}) is expired (was valid until ${ELEM_END_DATE}, ${DAYS_AGO})" if [ -z "${CN_EXPIRED_CRITICAL}" ]; then CN_EXPIRED_CRITICAL="${CN_EXPIRED_TMP}" else CN_EXPIRED_CRITICAL="${CN_EXPIRED_CRITICAL} ${CN_EXPIRED_TMP}" fi if [ -z "${CN}" ]; then CN='unavailable' fi add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 2" return 2 fi if [ -n "${CRITICAL_DAYS}" ] && [ -n "${CRITICAL_SECONDS}" ]; then debuglog "executing: ${OPENSSL} x509 -in /dev/stdin -noout -checkend ${CRITICAL_SECONDS} on cert element ${el_number} (${element_cn})" if ! echo "${1}" | ${OPENSSL} x509 -in /dev/stdin -noout -checkend "${CRITICAL_SECONDS}" >/dev/null; then debuglog "CRITICAL: certificate element ${el_number} (${element_cn}) will expire in ${ELEM_DAYS_VALID} day(s) on ${ELEM_END_DATE}" CN_EXPIRED_TMP="${element_cn}:${replace_current_message}:${OPENSSL_COMMAND} certificate element ${el_number} (${element_cn}) will expire in ${ELEM_DAYS_VALID} day(s) on ${ELEM_END_DATE}" if [ -z "${CN_EXPIRED_CRITICAL}" ]; then CN_EXPIRED_CRITICAL="${CN_EXPIRED_TMP}" else CN_EXPIRED_CRITICAL="${CN_EXPIRED_CRITICAL} ${CN_EXPIRED_TMP}" fi if [ -z "${CN}" ]; then CN='unavailable' fi add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 2" return 2 fi fi if [ -n "${WARNING_DAYS}" ] && [ -n "${WARNING_SECONDS}" ]; then debuglog "executing: ${OPENSSL} x509 -in /dev/stdin -noout -checkend ${WARNING_SECONDS} on cert element ${el_number}" if ! echo "${1}" | ${OPENSSL} x509 -in /dev/stdin -noout -checkend "${WARNING_SECONDS}" >/dev/null; then debuglog "WARNING: certificate element ${el_number} (${element_cn}) will expire in ${ELEM_DAYS_VALID} day(s) on ${ELEM_END_DATE}" CN_EXPIRED_TMP="${element_cn}:${replace_current_message}:${OPENSSL_COMMAND} certificate element ${el_number} (${element_cn}) will expire in ${ELEM_DAYS_VALID} day(s) on ${ELEM_END_DATE}" if [ -z "${CN_EXPIRED_WARNING}" ]; then CN_EXPIRED_WARNING="${CN_EXPIRED_TMP}" else CN_EXPIRED_WARNING="${CN_EXPIRED_WARNING} ${CN_EXPIRED_TMP}" fi if [ -z "${CN}" ]; then CN='unavailable' fi add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 1" return 1 fi fi if [ -n "${NOT_VALID_LONGER_THAN}" ]; then debuglog "checking if the certificate is valid longer than ${NOT_VALID_LONGER_THAN} days" debuglog " valid for ${DAYS_VALID} days" if compare "${DAYS_VALID}" '>' "${NOT_VALID_LONGER_THAN}"; then debuglog "Certificate expires in ${DAYS_VALID} days which is more than ${NOT_VALID_LONGER_THAN} days" prepend_critical_message "Certificate expires in ${DAYS_VALID} days which is more than ${NOT_VALID_LONGER_THAN} days" "${replace_current_message}" add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 2" return 2 fi fi elif [ "${OPENSSL_COMMAND}" = "crl" ]; then # CRL certificates # We always check expired certificates if compare "${ELEM_SECONDS_VALID}" '<' 1; then prepend_critical_message "${OPENSSL_COMMAND} certificate element ${el_number} (${element_cn}) is expired (was valid until ${ELEM_END_DATE})" "${replace_current_message}" add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 2" return 2 fi if [ -n "${CRITICAL_DAYS}" ] && [ -n "${CRITICAL_SECONDS}" ]; then # When comparing, always use values in seconds, because values in days might be floating point numbers if compare "${ELEM_SECONDS_VALID}" '<' "${CRITICAL_SECONDS}"; then prepend_critical_message "${OPENSSL_COMMAND} certificate element ${el_number} (${element_cn}) will expire in ${ELEM_DAYS_VALID} day(s) on ${ELEM_END_DATE}" "${replace_current_message}" if [ -z "${CN}" ]; then CN='unavailable' fi add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 2" return 2 fi fi if [ -n "${WARNING_DAYS}" ] && [ -n "${WARNING_SECONDS}" ]; then # When comparing, always use values in seconds, because values in days might be floating point numbers if compare "${ELEM_SECONDS_VALID}" '<' "${WARNING_SECONDS}"; then append_warning_message "${OPENSSL_COMMAND} certificate element ${el_number} (${element_cn}) will expire in ${ELEM_DAYS_VALID} day(s) on ${ELEM_END_DATE}" "${replace_current_message}" if [ -z "${CN}" ]; then CN='unavailable' fi add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 1" return 1 fi fi fi if [ -z "${CN}" ]; then CN='unavailable' fi # the element is valid: add to the list of valid CNs if [ -z "${CN_OK}" ]; then CN_OK="${element_cn}" else CN_OK="${CN_OK} ${element_cn}" fi verboselog "Certificate element ${el_number} (${element_cn}) is valid for ${ELEM_DAYS_VALID} days" add_prometheus_valid_output_line "cert_valid_chain_elem{cn=\"${CN}\", element=\"${el_number}\"} 0" } ################################################################################ # Converts SSL Labs or nmap grades to a numeric value # (see https://www.ssllabs.com/downloads/SSL_Server_Rating_Guide.pdf and # https://nmap.org/nsedoc/scripts/ssl-enum-ciphers.html) # Params # $1 program name # Sets NUMERIC_SSL_LAB_GRADE convert_grade() { GRADE="$1" unset NUMERIC_SSL_LAB_GRADE case "${GRADE}" in 'A+' | 'a+') # Value not in documentation NUMERIC_SSL_LAB_GRADE=85 shift ;; A | a | strong | Strong) NUMERIC_SSL_LAB_GRADE=80 shift ;; 'A-' | 'a-') # Value not in documentation NUMERIC_SSL_LAB_GRADE=75 shift ;; B | b) NUMERIC_SSL_LAB_GRADE=65 shift ;; C | c | weak | Weak) NUMERIC_SSL_LAB_GRADE=50 shift ;; D | d) NUMERIC_SSL_LAB_GRADE=35 shift ;; E | e) NUMERIC_SSL_LAB_GRADE=20 shift ;; F | f) NUMERIC_SSL_LAB_GRADE=0 shift ;; T | t | unknown | Unknown) # No trust: value not in documentation NUMERIC_SSL_LAB_GRADE=0 shift ;; M | m) # Certificate name mismatch: value not in documentation NUMERIC_SSL_LAB_GRADE=0 shift ;; *) unknown "Cannot convert SSL Lab grade ${GRADE}" ;; esac } ################################################################################ # Check if the specified host is an IP (does not check the validity # $1 string to check is_ip() { ARGUMENT=$1 debuglog "Checking if ${ARGUMENT} is an IP address" if [ "${ARGUMENT}" != "${ARGUMENT#*[0-9].[0-9]}" ]; then debuglog "${ARGUMENT} is an IPv4 address" echo '1' elif [ "${ARGUMENT}" != "${ARGUMENT#*:[0-9a-fA-F]}" ]; then debuglog "${ARGUMENT} is an IPv6 address" echo '1' else debuglog "${ARGUMENT} is not an IP address" echo '0' fi } ################################################################################ # Tries to fetch the certificate fetch_certificate() { RET=0 # IPv6 addresses need brackets in a URI if [ "${HOST_ADDR}" != "${HOST_ADDR#*[0-9].[0-9]}" ]; then debuglog "${HOST_ADDR} is an IPv4 address" elif [ "${HOST_ADDR}" != "${HOST_ADDR#*:[0-9a-fA-F]}" ]; then debuglog "${HOST_ADDR} is an IPv6 address" if [ -z "${HOST_ADDR##*\[*}" ]; then debuglog "${HOST_ADDR} is already specified with brackets" else debuglog "adding brackets to ${HOST_ADDR}" HOST="[${HOST_ADDR}]" fi else debuglog "${HOST_ADDR} is not an IP address" fi if [ -n "${REQUIRE_OCSP_STAPLING}" ]; then STATUS='-status' fi debuglog "fetch_certificate: PROTOCOL = ${PROTOCOL}" TIMEOUT_REASON="fetching certificate" # with TDS we run a python script to fetch the certificate and then continue as with --file if [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" = 'tds' ]; then get_tds_certificate "${HOST}" "${PORT}" # we switch to a 'file check' PROTOCOL= fi # Check if a protocol was specified (if not HTTP switch to TLS) if [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" != 'http' ] && [ "${PROTOCOL}" != 'https' ] && [ "${PROTOCOL}" != 'h2' ] && [ "${PROTOCOL}" != 'h3' ]; then case "${PROTOCOL}" in pop3 | ftp) exec_with_timeout "printf 'QUIT\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; pop3s | ftps) exec_with_timeout "printf 'QUIT\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; smtp) exec_with_timeout "printf 'QUIT\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} ${S_CLIENT_NAME} 2> ${ERROR} 1> ${CERT}" RET=$? ;; smtps) exec_with_timeout "printf 'QUIT\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} ${S_CLIENT_NAME} 2> ${ERROR} 1> ${CERT}" RET=$? ;; irc | ldap) exec_with_timeout "echo | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; ircs | ldaps | dns | sips) exec_with_timeout "echo | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; imap) exec_with_timeout "printf 'A01 LOGOUT\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; imaps) exec_with_timeout "printf 'A01 LOGOUT\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; mqtts) # https://stackoverflow.com/questions/58936653/problem-using-mosquitto-broker-with-netcat # we create a temporary file with the message content (because of quoting and special characters create_temporary_file MQTT_MESSAGE=${TEMPFILE} printf "\\x10\\x0d\\x00\\x04MQTT\\x04\\x00\\x00\\x00\\x00\\x01a" > "${MQTT_MESSAGE}" exec_with_timeout "cat ${MQTT_MESSAGE} | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; postgres | postgresql) exec_with_timeout "printf 'X\\0\\0\\0\\4' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; sieve) exec_with_timeout "echo 'LOGOUT' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; xmpp | xmpp-server) exec_with_timeout "echo 'Q' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${XMPPHOST} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; mysql) exec_with_timeout "echo | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -showcerts -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? ;; *) unknown "Error: unsupported protocol ${PROTOCOL}" ;; esac elif [ -n "${FILE}" ]; then debuglog "check if we have to convert the file ${FILE} to PEM" TYPE_TMP="$(${FILE_BIN} -L -b "${FILE}" | sed 's/.*://')" debuglog "certificate type (1): ${TYPE_TMP}" create_temporary_file CONVERSION_ERROR=${TEMPFILE} if echo "${FILE}" | "${GREP_BIN}" -q -E '[.](p12|pkcs12|pfx)$'; then debuglog 'converting PKCS #12 to PEM' if [ -n "${PASSWORD_SOURCE}" ]; then debuglog "executing ${OPENSSL} pkcs12 -in ${FILE} -out ${CERT} -nokeys -passin ${PASSWORD_SOURCE}" "${OPENSSL}" pkcs12 -in "${FILE}" -out "${CERT}" -nokeys -passin "${PASSWORD_SOURCE}" 2>"${CONVERSION_ERROR}" else debuglog "executing ${OPENSSL} pkcs12 -in ${FILE} -out ${CERT} -nokeys" "${OPENSSL}" pkcs12 -in "${FILE}" -out "${CERT}" -nokeys 2>"${CONVERSION_ERROR}" fi if [ $? -eq 1 ]; then CONVERSION_ERROR_TMP="$(head -n 1 "${CONVERSION_ERROR}")" unknown "Error converting ${FILE}: ${CONVERSION_ERROR_TMP}" fi elif [ -n "${JKSALIAS}" ] && "${FILE_BIN}" -L -b "${FILE}" | "${GREP_BIN}" -q -E 'KeyStore|data'; then debuglog 'converting JKS to PEM' check_required_prog 'keytool' KEYTOOLBIN=${PROG} if [ -z "${PASSWORD_SOURCE}" ]; then unknown "--jks-alias requires a password" fi debuglog "Executing ${KEYTOOLBIN} -exportcert -rfc -keystore ${FILE} -alias ${JKSALIAS} -file ${CERT} -storepass ${PASSWORD_SOURCE}" CONVERSION_ERROR=$("${KEYTOOLBIN}" -exportcert -rfc -keystore "${FILE}" -alias "${JKSALIAS}" -file "${CERT}" -storepass "${PASSWORD_SOURCE}" 2>&1) RET=$? if [ "${RET}" -eq 1 ]; then CONVERSION_ERROR_TMP="$(echo "${CONVERSION_ERROR}" | head -n 1)" unknown "Error converting JKS to PEM: ${CONVERSION_ERROR_TMP}" fi elif "${FILE_BIN}" -L -b "${FILE}" | "${GREP_BIN}" -q -E '(data|Certificate)'; then debuglog 'converting DER to PEM' "${OPENSSL}" x509 -inform der -in "${FILE}" -out "${CERT}" 2>"${CONVERSION_ERROR}" if [ $? -eq 1 ]; then CONVERSION_ERROR_TMP="$(head -n 1 "${CONVERSION_ERROR}")" debuglog "Error converting ${FILE}: ${CONVERSION_ERROR_TMP}" debuglog "Checking if ${FILE} is DER encoded CRL" if "${OPENSSL}" crl -in "${FILE}" -inform DER 2>/dev/null | "${GREP_BIN}" -F -q "BEGIN X509 CRL"; then debuglog "The input file is a CRL in DER format: converting to PEM" debuglog "Executing ${OPENSSL} crl -inform der -in ${FILE} -out ${CERT} 2> /dev/null" "${OPENSSL}" crl -inform der -in "${FILE}" -out "${CERT}" 2>"${CONVERSION_ERROR}" if [ $? -eq 1 ]; then CONVERSION_ERROR_TMP="$(head -n 1 "${CONVERSION_ERROR}")" unknown "Error converting CRL ${FILE}: ${CONVERSION_ERROR_TMP}" fi else CONVERSION_ERROR_TMP="$(head -n 1 "${CONVERSION_ERROR}")" unknown "Error converting ${FILE}: ${CONVERSION_ERROR_TMP}" fi fi else debuglog "Copying the certificate to ${CERT}" /bin/cat "${FILE}" >"${CERT}" RET=$? fi unset TIMEOUT_REASON debuglog "storing the certificate to ${CERT}" TYPE_TMP="$(${FILE_BIN} -L -b "${CERT}" | sed 's/.*://')" debuglog "certificate type (2): ${TYPE_TMP}" if ! "${GREP_BIN}" -q -F 'CRL' "${CERT}"; then NUM_CERTIFICATES=$("${GREP_BIN}" -F -c -- "-BEGIN CERTIFICATE-" "${CERT}") if [ "${NUM_CERTIFICATES}" -gt 1 ]; then debuglog "Certificate seems to be ca ca-bundle, splitting it" create_temporary_file USER_CERTIFICATE=${TEMPFILE} sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' "${CERT}" | awk -v n="1" '/-BEGIN CERTIFICATE-/{l++} (l==n) {print}' >"${USER_CERTIFICATE}" create_temporary_file INTERMEDIATE_CERTIFICATES=${TEMPFILE} sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' "${CERT}" | awk -v n="2" '/-BEGIN CERTIFICATE-/{l++} (l>=n) {print}' >"${INTERMEDIATE_CERTIFICATES}" VERIFY_COMMAND="${OPENSSL} verify ${ROOT_CA} -untrusted ${INTERMEDIATE_CERTIFICATES} ${USER_CERTIFICATE}" else debuglog "Certificate does not contain any intermediates, checking the chain will probably fail." VERIFY_COMMAND="${OPENSSL} verify ${ROOT_CA} ${CERT}" fi # verify the local certificate debuglog "verifying the certificate" debuglog " ${VERIFY_COMMAND} 2> ${ERROR} 1>&2" # on older versions of OpenSSL write the error on standard input # shellcheck disable=SC2086 ${VERIFY_COMMAND} 2>"${ERROR}" 1>&2 RET=$? else debuglog "skipping verification on CRL" fi else if [ "${PROTOCOL}" = 'h2' ]; then ALPN="-alpn h2" elif [ "${PROTOCOL}" = 'h3' ]; then ALPN="-alpn h3" fi exec_with_timeout "printf '${HTTP_REQUEST}' | ${OPENSSL} s_client ${QUIC} ${SECURITY_LEVEL} ${INETPROTO} ${CLIENT} ${CLIENTPASS} -crlf ${ALPN} -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -showcerts -verify 6 ${ROOT_CA} ${SSL_VERSION} ${SSL_VERSION_DISABLED} ${SSL_AU} ${STATUS} ${DANE} ${IGNOREEOF} ${RENEGOTIATION} 2> ${ERROR} 1> ${CERT}" RET=$? fi debuglog "Return value of the command = ${RET}" if [ -n "${DEBUG_CERT}" ]; then debuglog "storing a copy of the retrieved certificate in ${HOST_NAME}.crt for debugging purposes" open_for_writing "${HOST_NAME}.crt" cp "${CERT}" "${HOST_NAME}.crt" debuglog "storing a text copy of the retrieved certificate in ${HOST_NAME}.crt.txt for debugging purposes" open_for_writing "${HOST_NAME}.crt.txt" "${OPENSSL}" x509 -in "${HOST_NAME}.crt" -text -out "${HOST_NAME}.crt.txt" debuglog "storing a copy of the OpenSSL errors in ${HOST_NAME}.error for debugging purposes" open_for_writing "${HOST_NAME}.error" cp "${ERROR}" "${HOST_NAME}.error" fi if [ -n "${PROTOCOL}" ]; then protocol_tmp=":${PROTOCOL}" fi if [ "${RET}" -ne 0 ]; then MESSAGE_TMP="$(sed 's/^/SSL error: /' "${ERROR}")" if [ "${DEBUG}" -gt 0 ]; then debuglog "MESSAGE_TMP=" echo "${MESSAGE_TMP}" | sed 's/^/[DBG] /' fi # s_client could verify the server certificate because the server requires a client certificate if ascii_grep '^Client Certificate Types' "${CERT}"; then verboselog "Warning: the server requires a client certificate" elif ascii_grep 'nodename[ ]nor[ ]servname[ ]provided,[ ]or[ ]not[ ]known' "${ERROR}" || ascii_grep 'Name or service not known' "${ERROR}" || ascii_grep 'connect[ ]argument[ ]or[ ]target[ ]parameter[ ]malformed[ ]or[ ]ambiguous' "${ERROR}"; then ERROR="${HOST_ADDR} is not a valid hostname" prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep '500 Unable to connect' "${ERROR}"; then # tinyproxy delivers a different error message: harmonize ERROR="Cannot connect to ${HOST} on port ${PORT}" prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'Connection[ ]refused' "${ERROR}"; then ERROR='Connection refused' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'No route to host' "${ERROR}"; then ERROR='No route to host' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'Connection timed out' "${ERROR}"; then ERROR='OpenSSL connection timed out' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'unable to get local issuer certificate' "${ERROR}"; then if [ -z "${IGNORE_INCOMPLETE_CHAIN}" ]; then prepend_critical_message 'Error verifying the certificate chain (missing local issuer certificate)' fi elif ascii_grep 'self[ -]signed certificate' "${ERROR}"; then debuglog "self-signed certificate" if [ -z "${SELFSIGNED}" ]; then prepend_critical_message 'Self signed certificate' fi elif ascii_grep 'quic_do_handshake' "${ERROR}"; then prepend_critical_message 'QUIC not supported' elif ascii_grep 'dh[ ]key[ ]too[ ]small' "${ERROR}"; then prepend_critical_message 'DH with a key too small' elif ascii_grep 'alert[ ]handshake[ ]failure' "${ERROR}"; then prepend_critical_message 'Handshake failure' elif ascii_grep 'wrong[ ]version[ ]number' "${ERROR}"; then prepend_critical_message 'No TLS connection possible' elif ascii_grep 'tlsv1 alert decode error' "${ERROR}"; then prepend_critical_message 'Error decoding certificate' elif ascii_grep 'excessive message size' "${ERROR}"; then prepend_critical_message 'Error fetching the certificate (excessive message size)' elif ascii_grep 'gethostbyname failure' "${ERROR}"; then ERROR='Invalid host name' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'Operation[ ]timed[ ]out' "${ERROR}"; then ERROR='OpenSSL timed out' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'BIO_lookup_ex' "${ERROR}"; then ERROR='Unknown host' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep 'write:errno=54' "${ERROR}"; then ERROR='No certificate returned (SNI required?)' prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep "Didn't find STARTTLS in server response, trying anyway..." "${ERROR}"; then ERROR="Didn't find STARTTLS in server response" prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep "unsafe legacy renegotiation disabled" "${ERROR}"; then ERROR="The server does not support the Renegotiation Indication Extension" prepend_critical_message "${ERROR}" critical "SSL_CERT CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep "no certificate or crl found" "${ERROR}"; then ERROR="Cannot read or parse the supplied certificate (e.g., root certificate)" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${HOST_NAME}${protocol_tmp}: ${ERROR}" elif ascii_grep "Unable to load certificate file" "${ERROR}"; then # we handle TDS retried certs differently if [ "${HOST}" = 'localhost' ]; then ERROR="Cannot read certificate file" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${FILE}: ${ERROR}" else ERROR="Cannot fetch certificate" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${HOST}: ${ERROR}" fi elif ascii_grep "tlsv1 alert protocol version" "${ERROR}"; then ERROR="Unsupported protocol" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${FILE}: ${ERROR}" elif ascii_grep ":error:0A0C0103:SSL" "${ERROR}"; then ERROR="Legacy signature algorithm unsupported or disallowed" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${HOST}: ${ERROR}" elif ascii_grep ":ssl_choose_client_version:unsupported" "${ERROR}"; then ERROR="Unsupported TLS protocol version" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${HOST}: ${ERROR}" elif ascii_grep "unexpected eof while reading" "${ERROR}" || ascii_grep "ssl handshake failure" "${ERROR}"; then ERROR="TLS handshake error" prepend_critical_message "${ERROR}" critical "SSL_CERT_CRITICAL ${HOST_NAME}:${PORT}: ${ERROR}" else # Try to clean up the error message # Remove the 'verify and depth' lines # Take the 1st line (seems OK with the use cases I tested) ERROR_MESSAGE=$( "${GREP_BIN}" -v '^depth' "${ERROR}" | "${GREP_BIN}" -v '^verify' | head -n 1 ) debuglog "Unknown error: ${ERROR_MESSAGE}" prepend_critical_message "SSL error: ${ERROR_MESSAGE}" fi else if ascii_grep usage "${ERROR}" && [ "${PROTOCOL}" = "ldap" ]; then critical "it seems that OpenSSL -starttls does not yet support LDAP" fi NEGOTIATED_PROTOCOL=$("${GREP_BIN}" -F 'ALPN protocol' "${CERT}" | sed 's/^ALPN protocol: //') debuglog "Negotiated protocol: ${NEGOTIATED_PROTOCOL}" # check if the protocol was really HTTP/2 if [ "${PROTOCOL}" = 'h2' ]; then if ! "${GREP_BIN}" -q -F 'ALPN protocol: h2' "${CERT}"; then prepend_critical_message 'The server does not support HTTP/2' fi elif [ "${PROTOCOL}" = 'h3' ]; then if ! "${GREP_BIN}" -q -F 'ALPN protocol: h3' "${CERT}"; then prepend_critical_message 'The server does not support HTTP/3' fi fi fi } ################################################################################ # Adds metric to performance data # Params # $1 performance data in Nagios plugin format, # see https://nagios-plugins.org/doc/guidelines.html#AEN200 add_performance_data() { if [ -z "${PERFORMANCE_DATA}" ]; then PERFORMANCE_DATA="|${1}" else PERFORMANCE_DATA="${PERFORMANCE_DATA} $1" fi } ################################################################################ # Prepares sed-style command for variable replacement # Params # $1 variable name (e.g. SHORTNAME) # $2 variable value (e.g. SSL_CERT) var_for_sed() { VALUE_TMP="$(echo "$2" | sed -e 's#|#\\\\|#g')" echo "s|%$1%|${VALUE_TMP}|g" } ################################################################################ # Performs a grep removing the NULL characters first # # As the POSIX grep does not have the -a option, we remove the NULL characters # first to avoid the error Binary file matches # # Params # $1 pattern # $2 file # ascii_grep() { LC_ALL=C tr -d '\000' <"$2" | "${GREP_BIN}" -q "$1" } ################################################################################ # Checks if there is an option argument (should not begin with -) # # Params # $1 name of the option (e.g., '-w,--warning') to be used in the error message # $2 next command line parameter check_option_argument() { # shellcheck disable=SC2295 if [ -z "$2" ] || [ "${2%${2#?}}"x = '-x' ]; then unknown "'${1}' requires an argument" fi } ################################################################################ # Parse command line options # # Params # $* options parse_command_line_options() { COMMAND_LINE_ARGUMENTS=$* while true; do case "$1" in ######################################## # Options without arguments -A | --noauth) NOAUTH=1 shift ;; --all) ALL=1 shift ;; --all-local) ALL_LOCAL=1 shift ;; --allow-empty-san) REQUIRE_SAN="" shift ;; --altnames) ALTNAMES=1 deprecated "--altnames" "Enabled by default" shift ;; --check-ciphers-warnings) CHECK_CIPHERS_WARNINGS=1 shift ;; --check-http-headers) REQUIRED_HTTP_HEADERS="${DEFAULT_REQUIRED_HTTP_HEADERS}" UNREQUIRED_HTTP_HEADERS="${DEFAULT_UNREQUIRED_HTTP_HEADERS}" shift ;; --check-chain) CHECK_CHAIN=1 shift ;; --crl) CRL=1 shift ;; -d | --debug) DEBUG=$((DEBUG + 1)) shift ;; --debug-cert) DEBUG_CERT=1 shift ;; --debug-headers) DEBUG_HEADERS=1 shift ;; --debug-time) # start time DEBUG_TIME=$(date +%s) # --debug-time does not make any sense without -d if [ "${DEBUG}" -le 1 ] ; then DEBUG=1 fi shift ;; --default-format) echo "${DEFAULT_FORMAT}" exit ;; --do-not-resolve) DO_NOT_RESOLVE=1 shift ;; # DTLS --dtls) if [ -n "${SSL_VERSION}" ]; then unknown "--dtls: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi require_s_client_option '-dtls' SSL_VERSION="-dtls" shift ;; --dtls1) if [ -n "${SSL_VERSION}" ]; then unknown "--dtls1: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi require_s_client_option '-dtls1' SSL_VERSION="-dtls1" shift ;; --dtls1_2) if [ -n "${SSL_VERSION}" ]; then unknown "--dtls1_2: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi require_s_client_option '-dtls1_2' SSL_VERSION="-dtls1_2" shift ;; -h | --help | -\?) usage ;; --first-element-only) FIRST_ELEMENT_ONLY=1 shift ;; --force-dconv-date) FORCE_DCONV_DATE=1 shift ;; --force-perl-date) FORCE_PERL_DATE=1 shift ;; --http-use-get) HTTP_METHOD="GET" shift ;; --ignore-exp) NOEXP=1 shift ;; --ignore-altnames) ALTNAMES= shift ;; --ignore-http-headers) IGNORE_HTTP_HEADERS=1 shift ;; --ignore-host-cn) IGNORE_HOST_CN=1 shift ;; --ignore-sig-alg) NOSIGALG=1 shift ;; --ignore-sct) SCT= shift ;; --ignore-ssl-labs-cache) IGNORE_SSL_LABS_CACHE="&startNew=on" shift ;; --ignore-ssl-labs-errors) IGNORE_SSL_LABS_ERRORS=1 shift ;; --ignore-tls-renegotiation) IGNORE_TLS_RENEGOTIATION='1' shift ;; --ignore-unexpected-eof) IGNORE_UNEXPECTED_EOF='1' shift ;; --info) INFO='1' shift ;; --init-host-cache) INIT_HOST_CACHE=1 if ! [ -f "${HOST_CACHE}" ]; then debuglog "Initializing host cache" if ! touch "${HOST_CACHE}"; then unknown "Cannot create host cache ${HOST_CACHE}" fi fi shift ;; --nmap-with-proxy) NMAP_WITH_PROXY=1 shift ;; --no-perf) NO_PERF=1 shift ;; --no-proxy) NO_PROXY=1 shift ;; --no-proxy-s_client) NO_PROXY_S_CLIENT=1 shift ;; --no-proxy-curl) NO_PROXY_CURL=1 shift ;; --no-ssl2 | --no_ssl2) if [ "$1" = '--no_ssl2' ]; then deprecated "$1" "Use '--no-ssl2'" fi # we keep the old variant for compatibility SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_ssl2" shift ;; --no-ssl3 | --no_ssl3) if [ "$1" = '--no_ssl3' ]; then deprecated "$1" "Use '--no-ssl3'" fi # we keep the old variant for compatibility SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_ssl3" shift ;; --no-tls1 | --no_tls1) if [ "$1" = '--no_tls1' ]; then deprecated "$1" "Use '--no-tls1'" fi # we keep the old variant for compatibility SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1" shift ;; --no-tls1_1 | --no_tls1_1) if [ "$1" = '--no_tls1_1' ]; then deprecated "$1" "Use '--no-tls1_1'" fi # we keep the old variant for compatibility SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1_1" shift ;; --no-tls1_2 | --no_tls1_2) if [ "$1" = '--no_tls1_2' ]; then deprecated "$1" "Use '--no-tls1_2'" fi # we keep the old variant for compatibility SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1_2" shift ;; --no-tls1_3 | --no_tls1_3) if [ "$1" = '--no_tls1_3' ]; then deprecated "$1" "Use '--no-tls1_3'" fi # we keep the old variant for compatibility SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_tls1_3" shift ;; # deprecated: this is enabled by default! -N | --host-cn) deprecated "$1" 'Enabled by default' # __HOST__ is a placeholder for the specified host name which is always checked NAMES_TO_BE_CHECKED="__HOST__" shift ;; --prometheus) PROMETHEUS=1 shift ;; --quic) QUIC='-quic' shift ;; -q | --quiet) QUIET=1 shift ;; --rsa) RSA=1 shift ;; --require-dnssec) REQUIRE_DNSSEC=1 shift ;; --require-hsts) deprecated "--require-hsts" "Use '--require-http-header strict-transport-security'" add_required_header 'strict-transport-security' shift ;; --require-no-ssl2) DISALLOWED_PROTOCOLS="${DISALLOWED_PROTOCOLS} SSLv2" shift ;; --require-no-ssl3) DISALLOWED_PROTOCOLS="${DISALLOWED_PROTOCOLS} SSLv3" shift ;; --require-no-tls1) DISALLOWED_PROTOCOLS="${DISALLOWED_PROTOCOLS} TLSv1.0" shift ;; --require-no-tls1_1) DISALLOWED_PROTOCOLS="${DISALLOWED_PROTOCOLS} TLSv1.1" shift ;; --require-ocsp-stapling) REQUIRE_OCSP_STAPLING=1 shift ;; --require-purpose-critical) REQUIRE_PURPOSE_CRITICAL=1 shift ;; --require-san) deprecated "$1" "Enabled by default" REQUIRE_SAN=1 shift ;; --require-security-headers) deprecated "$1" "Use --check-http-headers" REQUIRED_HTTP_HEADERS="${DEFAULT_REQUIRED_HTTP_HEADERS}" shift ;; -s | --selfsigned) SELFSIGNED=1 shift ;; --ecdsa) ECDSA=1 shift ;; --ssl2) if [ -n "${SSL_VERSION}" ]; then unknown "--ssl2: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi SSL_VERSION="-ssl2" shift ;; --ssl3) if [ -n "${SSL_VERSION}" ]; then unknown "--ssl3: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi SSL_VERSION="-ssl3" shift ;; --tls1) if [ -n "${SSL_VERSION}" ]; then unknown "--tls1: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi SSL_VERSION="-tls1" shift ;; --tls1_1) if [ -n "${SSL_VERSION}" ]; then unknown "--tls1_1: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi SSL_VERSION="-tls1_1" shift ;; --tls1_2) if [ -n "${SSL_VERSION}" ]; then unknown "--tls1_2: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi SSL_VERSION="-tls1_2" shift ;; --tls1_3) if [ -n "${SSL_VERSION}" ]; then unknown "--tls1_3: only one protocol can be specified at the same time (${SSL_VERSION} is already specified)" fi SSL_VERSION="-tls1_3" shift ;; --ocsp) deprecated '--ocsp' "Enabled by default" # deprecated shift ;; --ignore-incomplete-chain) IGNORE_INCOMPLETE_CHAIN=1 shift ;; --ignore-maximum-validity) IGNORE_MAXIMUM_VALIDITY=1 shift ;; --ignore-ocsp) OCSP="" shift ;; --ignore-ocsp-errors) OCSP_IGNORE_ERRORS=1 shift ;; --ignore-ocsp-timeout) OCSP_IGNORE_TIMEOUT=1 shift ;; --terse) TERSE=1 shift ;; -v | --verbose) VERBOSE=$((VERBOSE + 1)) shift ;; -V | --version) echo "check_ssl_cert version ${VERSION}" exit "${STATUS_UNKNOWN}" ;; -4) INETPROTO="-4" shift ;; -6) INETPROTO="-6" shift ;; ######################################## # Options with one argument -c | --critical) check_option_argument '-c,--critical' "$2" check_float "$2" "--critical: the number of days should be an integer or a float" CRITICAL_DAYS="$2" CRITICAL_SECONDS=$(days_to_seconds "${CRITICAL_DAYS}") shift 2 ;; --check-ciphers) check_option_argument '--check-ciphers' "$2" CHECK_CIPHERS="$2" shift 2 ;; --configuration) check_option_argument '--configuration' "$2" if [ -r "$2" ]; then # custom configuration file while IFS= read -r line; do # shellcheck disable=SC2086 set -- "$@" ${line} done <"$2" else unknown "Cannot read $2" fi shift 2 ;; --curl-bin) check_option_argument '--curl-bin' "$2" CURL_BIN="$2" shift 2 ;; --curl-user-agent) deprecated '--curl-user-agent' "Use '--user-agent'" check_option_argument '--curl-user-agent' "$2" HTTP_USER_AGENT="$2" shift 2 ;; --custom-http-header) check_option_argument '--custom-http-header' "$2" CUSTOM_HTTP_HEADER="$2" shift 2 ;; --date) check_option_argument '--date' "$2" DATEBIN="$2" shift 2 ;; # Deprecated option: used to be as --warning --days) deprecated '--days' "Use the '--warning' option" check_option_argument '--days' "$2" check_integer "$2" "--days: the number of days should be an integer" WARNING_DAYS="$2" WARNING_SECONDS=$(days_to_seconds "${WARNING_DAYS}") shift 2 ;; --debug-file) check_option_argument '--debug-file' "$2" DEBUG_FILE="$2" shift 2 ;; --dig-bin) check_option_argument '--dig-bin' "$2" DIG_BIN="$2" shift 2 ;; --path) check_option_argument '--path' "$2" export PATH="$2" shift 2 ;; --inetproto) check_option_argument '--inetproto' "$2" INETPROTO="-$2" shift 2 ;; --jks-alias) check_option_argument '--jks-alias' "$2" JKSALIAS="$2" shift 2 ;; --nmap-bin) check_option_argument '--nmap-bin' "$2" NMAP_BIN="$2" shift 2 ;; -e | --email) check_option_argument 'e|--email' "$2" ADDR="$2" shift 2 ;; -f | --file) check_option_argument ' -f|--file' "$2" FILE="$2" # remove _HOST_ from the names to be checked as no host was specified # we leave the remaining hosts specified with --cn NAMES_TO_BE_CHECKED=$(echo "${NAMES_TO_BE_CHECKED}" | sed 's/__HOST__ *//') ALTNAMES= shift 2 ;; --file-bin) check_option_argument '--file-bin' "$2" FILE_BIN="$2" shift 2 ;; --format) check_option_argument '--format' "$2" FORMAT="$2" shift 2 ;; --grep-bin) check_option_argument '--grep-bin' "$2" GREP_BIN="$2" shift 2 ;; -H | --host) check_option_argument '-H|--host' "$2" HOST="$2" # remove http[s] from the input if echo "${HOST}" | "${GREP_BIN}" -F -q '://'; then # try to remove the protocol (we do not consider URLs without # an authority (https://en.wikipedia.org/wiki/URL) debuglog "Stripping protocol and path from URL" HOST=$(echo "${HOST}" | sed 's/^[a-z]*:\/\///' | sed 's/\/.*//') fi # remove the trailing . from FQDN (otherwise the CN will not match) HOST=$(echo "${HOST}" | sed 's/[.]$//') shift 2 ;; -i | --issuer) check_option_argument '-i|--issuer' "$2" ISSUER="$2" shift 2 ;; --issuer-cert-cache) check_option_argument '--issuer-cert-cache' "$2" ISSUER_CERT_CACHE="$2" shift 2 ;; -L | --check-ssl-labs) check_option_argument '-L|--check-ssl-labs' "$2" SSL_LAB_CRIT_ASSESSMENT="$2" shift 2 ;; --check-ssl-labs-warn) check_option_argument '--check-ssl-labs-warn' "$2" SSL_LAB_WARN_ASSESTMENT="$2" shift 2 ;; --python-bin) check_option_argument '--python-bin' "$2" PYTHON_BIN="$2" shift 2 ;; --security-level) check_option_argument '--security-level' "$2" if ! echo "$2" | "${GREP_BIN}" -q '^[0-5]$' ; then unknown 'Invalid secuirity level' fi SECURITY_LEVEL="-cipher DEFAULT@SECLEVEL=$2" shift 2 ;; --serial) check_option_argument '--serial' "$2" SERIAL_LOCK="$2" shift 2 ;; --element) check_option_argument '--element' "$2" ELEMENT="$2" shift 2 ;; --skip-element) check_option_argument '--skip-element' "$2" if [ -z "${SKIP_ELEMENT}" ]; then SKIP_ELEMENT="$2" else SKIP_ELEMENT="${SKIP_ELEMENT}\\n$2" fi shift 2 ;; --require-http-header) check_option_argument '--require-http-header header' "$2" add_required_header "$2" shift 2 ;; --require-no-http-header) check_option_argument '--require-no-http-header header' "$2" add_unrequired_header "$2" shift 2 ;; --require-purpose) check_option_argument '--require-purpose' "$2" if [ -z "${REQUIRE_PURPOSE}" ]; then REQUIRE_PURPOSE="$2" else REQUIRE_PURPOSE="${REQUIRE_PURPOSE} $2" fi shift 2 ;; --require-security-header) deprecated '--require-security-header' "Use '--require-http-header' instead" check_option_argument '--require-security-header header' "$2" add_required_header "$2" shift 2 ;; --require-security-headers-path) check_option_argument '--require-security-header-path path' "$2" HTTP_HEADERS_PATH=$2 shift 2 ;; --fingerprint) check_option_argument '--fingerprint' "$2" FINGERPRINT_LOCK="$2" shift 2 ;; --fingerprint-alg) check_option_argument '--fingerprint-alg' "$2" FINGERPRINT_ALG="$2" shift 2 ;; --long-output) check_option_argument '--long-output' "$2" LONG_OUTPUT_ATTR="$2" shift 2 ;; -n | --cn | -m | --match) if [ "$1" = '-n' ]; then deprecated '-n' "Use '-m'" fi if [ "$1" = '--cn' ]; then deprecated '--cn' "Use '--match'" fi check_option_argument ' -n|--cn|-m|--match' "$2" if [ -z "${NAMES_TO_BE_CHECKED}" ]; then NAMES_TO_BE_CHECKED="${2}" else NAMES_TO_BE_CHECKED="${NAMES_TO_BE_CHECKED} ${2}" fi debuglog "--cn specified: NAMES_TO_BE_CHECKED = ${NAMES_TO_BE_CHECKED}" shift 2 ;; --not-issued-by) check_option_argument '--not-issued-by' "$2" NOT_ISSUED_BY="$2" shift 2 ;; --not-valid-longer-than) check_option_argument '--not-valid-longer-than' "$2" NOT_VALID_LONGER_THAN=$2 shift 2 ;; --ocsp-critical) check_option_argument '--ocsp-critical' "$2" OCSP_CRITICAL="$2" shift 2 ;; --ocsp-warning) check_option_argument '--ocsp-warning' "$2" OCSP_WARNING="$2" shift 2 ;; -o | --org) check_option_argument '-o|--org' "$2" ORGANIZATION="$2" shift 2 ;; --openssl) check_option_argument '--openssl' "$2" OPENSSL="$2" shift 2 ;; --password) check_option_argument '--password' "$2" PASSWORD_SOURCE="$2" shift 2 ;; -p | --port) check_option_argument '-p|--port' "$2" PORT="$2" shift 2 ;; --precision) check_option_argument '--precision' "$2" check_integer "$2" "--precision: the precision should be an integer" SCALE="$2" shift 2 ;; -P | --protocol) check_option_argument '-P|--protocol' "$2" PROTOCOL="$2" # set the protocol to lowercase PROTOCOL=$( echo "${PROTOCOL}" | tr '[:upper:]' '[:lower:]' ) shift 2 ;; --proxy) check_option_argument '--proxy' "$2" PROXY="$2" export http_proxy="$2" shift 2 ;; --resolve) check_option_argument '--resolve' "$2" RESOLVE="$2" shift 2 ;; -r | --rootcert) check_option_argument '-r|--rootcert' "$2" ROOT_CA="$2" shift 2 ;; --rootcert-dir) check_option_argument '--rootcert-dir' "$2" ROOT_CA_DIR="$2" shift 2 ;; --rootcert-file) check_option_argument '--rootcert-file' "$2" ROOT_CA_FILE="$2" shift 2 ;; -C | --clientcert) check_option_argument '-C|--clientcert' "$2" CLIENT_CERT="$2" shift 2 ;; -K | --clientkey) check_option_argument '-K|--clientkey' "$2" CLIENT_KEY="$2" shift 2 ;; --clientpass) if [ $# -gt 1 ]; then CLIENT_PASS="$2" shift 2 else unknown "--clientpass requires an argument" fi ;; --sni) check_option_argument '--sni' "$2" SNI="$2" shift 2 ;; -S | --ssl) deprecated "$1" "Use '--ssl2' or '--ssl3' instead" check_option_argument '' "$2" if [ "$2" = "2" ] || [ "$2" = "3" ]; then SSL_VERSION="-ssl${2}" shift 2 else unknown "invalid argument for --ssl" fi ;; -t | --timeout) check_option_argument '-t|--timeout' "$2" check_integer "$2" "--timeout: the timeout should be an integer" TIMEOUT="$2" shift 2 ;; --temp) check_option_argument '--temp' "$2" TMPDIR="$2" shift 2 ;; -u | --url) check_option_argument '-u|--url' "$2" HTTP_REQUEST_URL="$2" shift 2 ;; --user-agent) check_option_argument '--user-agent' "$2" HTTP_USER_AGENT="$2" shift 2 ;; -w | --warning) check_option_argument '-w|--warning' "$2" check_float "$2" "--warning: the number of days should be an integer or a float" WARNING_DAYS="$2" WARNING_SECONDS=$(days_to_seconds "${WARNING_DAYS}") shift 2 ;; --xmpphost) check_option_argument '--xmpphost' "$2" XMPPHOST="$2" shift 2 ;; ############################## # Variable number of arguments --dane) if [ -n "${DANE}" ]; then unknown "--dane can be specified only once" fi # check the second parameter if it exist if [ $# -gt 1 ]; then # shellcheck disable=SC2295 if [ "${2%${2#?}}"x = '-x' ]; then DANE=1 shift else DANE=$2 shift 2 fi else DANE=1 shift fi ;; --maximum-validity) MAXIMUM_VALIDITY=397 # check the second optional parameter if it exist if [ $# -gt 1 ]; then # shellcheck disable=SC2295 if [ "${2%${2#?}}"x = '-x' ]; then shift else MAXIMUM_VALIDITY=$2 shift 2 fi else shift fi ;; --resolve-over-http) # dns.google.com: we use the IP in case DNS is not directly reachable RESOLVE_OVER_HTTP=8.8.8.8 # check the second optional parameter if it exist if [ $# -gt 1 ]; then # shellcheck disable=SC2295 if [ "${2%${2#?}}"x = '-x' ]; then shift else RESOLVE_OVER_HTTP=$2 shift 2 fi else shift fi ;; --require-client-cert) REQUIRE_CLIENT_CERT=1 # check the second optional parameter if it exist if [ $# -gt 1 ]; then # shellcheck disable=SC2295 if [ "${2%${2#?}}"x = '-x' ]; then shift else REQUIRE_CLIENT_CERT_CAS=$2 shift 2 fi else shift fi ;; --require-x-frame-options) deprecated '--require-x-frame-options' "Use '--require-http-header X-Frame-Options'" add_required_header X-Frame-Options # default path if [ -z "${HTTP_HEADERS_PATH}" ]; then HTTP_HEADERS_PATH='/' fi # check the second optional parameter if it exist if [ $# -gt 1 ]; then # shellcheck disable=SC2295 if [ "${2%${2#?}}"x = '-x' ]; then shift else HTTP_HEADERS_PATH=$2 shift 2 fi else shift fi ;; --ignore-connection-problems) # default OK IGNORE_CONNECTION_STATE="${STATUS_OK}" # check the second optional parameter if it exist if [ $# -gt 1 ]; then # shellcheck disable=SC2295 if [ "${2%${2#?}}"x = '-x' ]; then shift else IGNORE_CONNECTION_STATE=$2 shift 2 fi else shift fi ;; ######################################## # Special --) shift break ;; -*) # we try to check for grouped variables OPTION="${1}" # if the option begins with a single dash and it's longer than one character OPTION_TMP="$(echo "${OPTION}" | wc -c | sed 's/ //g')" if ! echo "${OPTION}" | "${GREP_BIN}" -q -- '^--' && [ "${OPTION_TMP}" -gt 3 ]; then if [ "${DEBUG}" -gt 0 ]; then echo "[DBG] unknown option ${OPTION}: splitting since it could be an option group" 1>&2 fi for letter in $(echo "${OPTION}" | sed 's/^-//' | "${GREP_BIN}" -o .); do parse_command_line_options "-${letter}" done shift else unknown "invalid option: ${1}" fi ;; *) if [ -n "$1" ]; then unknown "invalid option: ${1}" fi break ;; esac done } ################################################################################ # Main ################################################################################ main() { START_TIME=$(date +%s) ############################################################################## # we need grep from the beginning (will fix later if --grep-bin is specified) if [ -z "${GREP_BIN}" ]; then GREP_BIN=$(command -v grep) fi # Default values ALTNAMES=1 # enabled by default NAMES_TO_BE_CHECKED="__HOST__" # enabled by default CRITICAL_DAYS=15 CRITICAL_SECONDS=$(days_to_seconds "${CRITICAL_DAYS}") CRL="" CURL_BIN="" CURL_PROXY="" HTTP_USER_AGENT="check_ssl_cert/${VERSION}" CUSTOM_HTTP_HEADER="" DANE="" DEBUG="0" DIG_BIN="" DISALLOWED_PROTOCOLS="" ECDSA="" ELEMENT=0 FILE_BIN="" FORCE_DCONV_DATE="" FORCE_PERL_DATE="" FORMAT="" HTTP_METHOD="HEAD" HTTP_REQUEST_URL="/" IGNORE_SSL_LABS_CACHE="" NMAP_BIN="" NO_PROXY="" NO_PROXY_CURL="" NO_PROXY_S_CLIENT="" OCSP="1" # enabled by default OCSP_IGNORE_ERRORS="" OCSP_IGNORE_TIMEOUT="" PORT="" PROMETHEUS_OUTPUT_STATUS="" PROMETHEUS_OUTPUT_VALID="" PROMETHEUS_OUTPUT_DAYS="" PROXY="" REQUIRE_OCSP_STAPLING="" REQUIRE_SAN=1 RSA="" SCT="1" # enabled by default SKIP_ELEMENT="" SNI="" VERBOSE="0" WARNING_DAYS=20 WARNING_SECONDS=$(days_to_seconds "${WARNING_DAYS}") XMPPHOST="" if [ -z "${TIMEOUT}" ]; then TIMEOUT="120" fi # after 2020-09-01 we could set the default to 398 days because of Apple # https://support.apple.com/en-us/HT211025 NOT_VALID_LONGER_THAN="" FIRST_ELEMENT_ONLY="" # Set the default temp dir if not set if [ -z "${TMPDIR}" ]; then TMPDIR="/tmp" fi ################################################################################ # Process command line options # # We do not use getopts since it is unable to process long options and it is # Bash specific. # read additional options from the configuration file if [ -r "${CONFIGURATION_FILE}" ]; then while IFS= read -r line; do # shellcheck disable=SC2086 set -- "$@" ${line} done <"${CONFIGURATION_FILE}" fi parse_command_line_options "$@" if ! [ -x "${GREP_BIN}" ] ; then unknown "${GREP_BIN} in not executable" fi if [ "${DEBUG}" -ge 1 ]; then debuglog "check_ssl_cert version: ${VERSION}" UNAME_TMP="$(uname -a)" debuglog "System info: ${UNAME_TMP}" if [ -r /etc/os-release ]; then debuglog "/etc/os-release:" sed 's/^/[DBG] /' /etc/os-release 1>&2 fi OS="$(uname -s)" if [ "${OS}" = 'Darwin' ]; then debuglog "Darwin info:" sw_vers | sed 's/^/[DBG] /' 1>&2 fi USER_TMP="$(whoami)" debuglog "User: ${USER_TMP}" fi # process the --ignore-host-cn option by removing __HOST__ from the list of checked names if [ -n "${IGNORE_HOST_CN}" ] ; then NAMES_TO_BE_CHECKED=$(echo "${NAMES_TO_BE_CHECKED}" | sed 's/__HOST__ *//') fi ################################################################################ # Default ports if [ -z "${PORT}" ]; then if [ -z "${PROTOCOL}" ]; then # default is HTTPS PORT=443 else case "${PROTOCOL}" in smtp) PORT=25 ;; smtps) PORT=465 ;; pop3) PORT=110 ;; dns) PORT=853 ;; ftp | ftps) PORT=21 ;; pop3s) PORT=995 ;; irc | ircs) PORT=6667 ;; ldap) PORT=389 ;; ldaps) PORT=636 ;; imap) PORT=143 ;; imaps) PORT=993 ;; postgres | postgresql) PORT=5432 ;; sieve) PORT=4190 ;; http) PORT=80 ;; https | h2 | h3) PORT=443 ;; mqtts) PORT=8883 ;; mysql) PORT=3306 ;; sips) PORT=5061 ;; xmpp) PORT=5222 ;; xmpp-server) PORT=5269 ;; tds) PORT=1433 ;; *) unknown "Error: unsupported protocol ${PROTOCOL}" ;; esac fi fi if [ -n "${DEBUG_FILE}" ]; then open_for_appending "${DEBUG_FILE}" date >>"${DEBUG_FILE}" fi if [ "${DEBUG}" -gt 0 ]; then debuglog "Shell: ${SHELL}" SHELL_VERSION=$("${SHELL}" --version) echo "${SHELL_VERSION}" | sed 's/^/[DBG] /' 1>&2 # should take a look at # https://github.com/stephane-chazelas/misc-scripts/blob/master/which_interpreter debuglog "grep: ${GREP_BIN}" GREP_VERSION=$(${GREP_BIN} --version 2>&1) if echo "${GREP_VERSION}" | "${GREP_BIN}" -q BusyBox ; then # BusyBox grep does not have a -version option GREP_VERSION=$( echo "${GREP_VERSION}" | sed -e 's/.*BusyBox/BusyBox/' -e 's/\. Usage.*//' ) fi echo "${GREP_VERSION}" | sed 's/^/[DBG] /' 1>&2 HOSTNAME_BIN=$(command -v hostname) debuglog "hostname: ${HOSTNAME_BIN}" debuglog "\$PATH: ${PATH}" fi debuglog "Command line arguments: ${COMMAND_LINE_ARGUMENTS}" debuglog " TMPDIR = ${TMPDIR}" if [ -n "${ALL}" ]; then # enable ciphers checks (level A) SSL_LAB_CRIT_ASSESSMENT='A' fi if [ -n "${ALL_LOCAL}" ] || [ -n "${ALL}" ]; then # enable ciphers checks (level A) CHECK_CIPHERS='A' # enable ciphers warnings CHECK_CIPHERS_WARNINGS=1 if "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -- '-no_ssl2'; then debuglog "s_client supports -no_ssl2: disabling" # disable SSL 2.0 and SSL 3.0 SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_ssl2 -no_ssl3" else # disable SSL 3.0 (SSL 2.0 is not supported anymore) SSL_VERSION_DISABLED="${SSL_VERSION_DISABLED} -no_ssl3" fi # we check HTTP headers only with HTTP/HTTPS if [ -z "${IGNORE_HTTP_HEADERS}" ] ; then if [ -z "${PROTOCOL}" ] || [ "${PROTOCOL}" = 'http' ] || [ "${PROTOCOL}" = 'https' ] || [ "${PROTOCOL}" = 'h2' ]; then REQUIRED_HTTP_HEADERS="${DEFAULT_REQUIRED_HTTP_HEADERS}" UNREQUIRED_HTTP_HEADERS="${DEFAULT_UNREQUIRED_HTTP_HEADERS}" fi fi fi debuglog "Required HTTP headers: ${REQUIRED_HTTP_HEADERS}" debuglog "Unrequired HTTP headers: ${UNREQUIRED_HTTP_HEADERS}" ############################## # Check options: sanity checks if [ -z "${HOST}" ] && [ -z "${FILE}" ]; then if [ -z "${INIT_HOST_CACHE}" ]; then unknown "No host specified" else exit fi elif [ -z "${HOST}" ] && [ -n "${FILE}" ]; then HOST='localhost' fi # HTTP checks do only make sense with HTTP # do not split the next line otherwise ShellCheck will fail to parse it # see https://www.shellcheck.net/wiki/SC2235 for the { ; } syntax if [ -n "${FILE}" ] || { [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" != 'http' ] && [ "${PROTOCOL}" != 'https' ] && [ "${PROTOCOL}" != 'h2' ]; }; then if [ -n "${REQUIRED_HTTP_HEADERS}" ] || [ -n "${UNREQUIRED_HTTP_HEADERS}" ]; then unknown "HTTP headers can only be checked with HTTP[S]" fi fi ############################################################################## # curl if [ -z "${CURL_BIN}" ]; then if [ -n "${SSL_LAB_CRIT_ASSESSMENT}" ] || [ -n "${OCSP}" ] || [ -n "${CRL}" ] || [ -n "${IGNORE_CONNECTION_STATE}" ] || [ -n "${FILE_URI}" ]; then debuglog "curl binary needed. SSL Labs = ${SSL_LAB_CRIT_ASSESSMENT}, OCSP = ${OCSP}, CURL = ${CRL}, IGNORE_CONNECTION_STATE=${IGNORE_CONNECTION_STATE}, FILE_URI=${FILE_URI}" debuglog "curl binary not specified" check_required_prog curl CURL_BIN=${PROG} debuglog "curl available: ${CURL_BIN}" CURL_BIN_VERSION_TMP="$(${CURL_BIN} --version)" debuglog "${CURL_BIN_VERSION_TMP}" else debuglog "curl binary not needed. SSL Labs = ${SSL_LAB_CRIT_ASSESSMENT}, OCSP = ${OCSP}" fi else # we check if the provided binary actually works check_required_prog "${CURL_BIN}" fi ############################################################################## # OpenSSL if [ -n "${OPENSSL}" ]; then if [ ! -x "${OPENSSL}" ]; then unknown "${OPENSSL} is not an executable" fi else OPENSSL='openssl' fi check_required_prog "${OPENSSL}" OPENSSL=${PROG} if [ -n "${QUIC}" ] ; then require_s_client_option '-quic' # QUIC requires HTTP/2 if [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" != 'h3' ]; then critical 'QUIC only works with HTTP/2' else verboselog '--quic specified enabling HTTP/3' PROTOCOL='h3' fi # check if curl supports HTTP/3 if curl --help all | grep -q -- --http3 ; then debuglog 'curl supports HTTP/3' CURL_QUIC='--http3' fi fi ############################################################################## # custom grep if [ -z "${GREP_BIN}" ]; then GREP_BIN='grep' fi check_required_prog "${GREP_BIN}" GREP_BIN=${PROG} ############################################################################## # OpenSSL options if [ -n "${REQUIRE_PURPOSE}" ] || [ -n "${REQUIRE_PURPOSE_CRITICAL}" ]; then require_x509_option "-ext" " (required for certificate purpose)" fi ################################################################################ # Check if openssl s_client supports the -proxy option # SCLIENT_PROXY= SCLIENT_PROXY_ARGUMENT= CURL_PROXY= CURL_PROXY_ARGUMENT= if [ -n "${http_proxy}" ] || [ -n "${HTTP_PROXY}" ]; then debuglog "\$http_proxy is set: configuring the proxy settings" if [ -n "${http_proxy}" ]; then HTTP_PROXY="${http_proxy}" fi if [ -z "${https_proxy}" ]; then # try to set https_proxy https_proxy="${http_proxy}" fi if [ -z "${HTTPS_PROXY}" ]; then # try to set HTTPS_proxy HTTPS_PROXY="${HTTP_PROXY}" fi if [ -n "${CURL_BIN}" ] && ${CURL_BIN} --manual 2>&1 | "${GREP_BIN}" -F -q -- --proxy; then debuglog "Adding --proxy ${HTTP_PROXY} to the curl options" CURL_PROXY="--proxy" CURL_PROXY_ARGUMENT="${HTTP_PROXY}" fi if ${OPENSSL} s_client -help 2>&1 | "${GREP_BIN}" -F -q -- -proxy || ${OPENSSL} s_client not_a_real_option 2>&1 | "${GREP_BIN}" -F -q -- -proxy; then SCLIENT_PROXY="-proxy" SCLIENT_PROXY_ARGUMENT="$(echo "${HTTP_PROXY}" | sed 's/.*:\/\///' | sed 's/\/$//')" debuglog "Adding -proxy ${SCLIENT_PROXY_ARGUMENT} to the s_client options" else verboselog "'${OPENSSL} s_client' does not support '-proxy': HTTP_PROXY could be ignored" fi fi if [ -n "${NO_PROXY_CURL}" ]; then CURL_PROXY='' CURL_PROXY_ARGUMENT='' fi if [ -n "${NO_PROXY_S_CLIENT}" ]; then SCLIENT_PROXY='' SCLIENT_PROXY_ARGUMENT='' fi debuglog "Proxy settings (after):" debuglog " http_proxy = ${http_proxy}" debuglog " https_proxy = ${https_proxy}" debuglog " HTTP_PROXY = ${HTTP_PROXY}" debuglog " HTTPS_PROXY = ${HTTPS_PROXY}" debuglog " s_client = ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT}" debuglog " curl = ${CURL_PROXY} ${CURL_PROXY_ARGUMENT}" # nmap doesn't work properly behind a proxy. # # See e.g., # https://subscription.packtpub.com/book/networking-and-servers/9781786467454/2/ch02lvl1sec37/scanning-through-proxies # https://security.stackexchange.com/questions/120708/nmap-through-proxy # if [ -n "${http_proxy}" ] || [ -n "${https_proxy}" ] || [ -n "${HTTP_PROXY}" ] || [ -n "${HTTPS_PROXY}" ] || [ -n "${SCLIENT_PROXY}" ] || [ -n "${CURL_PROXY}" ]; then if [ -z "${NMAP_WITH_PROXY}" ] ; then DISABLE_NMAP=1 USING_A_PROXY=1 debuglog "A proxy is specified: nmap disabled" verboselog "A proxy is specified: nmap checks disabled" else debuglog "A proxy is specified: nmap enabled because of --nmap-with-proxy" verboselog "A proxy is specified: nmap enabled because of --nmap-with-proxy" fi fi # Expect (optional) EXPECT="$(command -v expect 2>/dev/null)" test -x "${EXPECT}" || EXPECT="" if [ -z "${EXPECT}" ]; then verboselog "expect not available" 2 else verboselog "expect available (${EXPECT})" 2 fi # Timeout (optional) TIMEOUT_BIN="$(command -v timeout 2>/dev/null)" test -x "${TIMEOUT_BIN}" || TIMEOUT_BIN="" if [ -z "${TIMEOUT_BIN}" ]; then verboselog "timeout not available" 2 else verboselog "timeout available (${TIMEOUT_BIN})" 2 fi if [ -z "${TIMEOUT_BIN}" ] && [ -z "${EXPECT}" ]; then verboselog "disabling timeouts" 2 fi ############################################################################## # Check if the host can be resolved if [ -n "${DO_NOT_RESOLVE}" ] && [ -n "${RESOLVE_OVER_HTTP}" ] ; then unknown "--do-not-resolve and --resolve-over-http cannot be specified at the same time" fi # we check only if # --do-not-resolve was not specified # --resolve was not specified (as the IP is supplied on the command line) if [ -z "${DO_NOT_RESOLVE}" ] && [ -z "${RESOLVE}" ] ; then if [ -z "${RESOLVE_OVER_HTTP}" ] ; then ETC_HOSTS= debuglog "Checking if the host is listed in /etc/hosts" if "${GREP_BIN}" -q "[[:blank:]]${HOST}[[:blank:]]*$" /etc/hosts ; then debuglog "Host listed in /etc/hosts as" etc_hosts_entry=$( "${GREP_BIN}" "[[:blank:]]${HOST}[[:blank:]]*$" /etc/hosts ) debuglog "${etc_hosts_entry}" if [ "${INETPROTO}" = '-4' ] ; then if grep -q "^[0-9.]*[[:blank:]]*${HOST}" /etc/hosts ; then ETC_HOSTS=4 fi elif [ "${INETPROTO}" = '-6' ] ; then if grep -q "^[a-fA-F:0-9]*[[:blank:]]*${HOST}" /etc/hosts ; then ETC_HOSTS=6 NMAP_INETPROTO=-6 fi elif grep -q "^[0-9.]*[[:blank:]]*${HOST}" /etc/hosts ; then ETC_HOSTS=4 elif grep -q "^[a-fA-F:0-9]*[[:blank:]]*${HOST}" /etc/hosts ; then ETC_HOSTS=6 NMAP_INETPROTO=-6 fi fi # if the host was not listed in /etc/hosts we check if there is a DNS record if [ -z "${ETC_HOSTS}" ] ; then debuglog "Host not found in /etc/hosts: checking DNS" # we need the FQDN of an host to check the CN # - the domain does not contain a . # - we are not checking a file # - we are not checking localhost # - we are not checking an IPv6 address (which does not have dots) RESOLVE_ERROR= if ! echo "${HOST}" | "${GREP_BIN}" -q '[.]' && [ -z "${FILE}" ] && [ "${HOST}" != 'localhost' ] && ! echo "${HOST}" | "${GREP_BIN}" -q -F ':'; then debuglog "Domain for ${HOST} missing" DOMAIN=$(nslookup "${HOST}" | "${GREP_BIN}" ^Name: | head -n 1 | cut -d. -f2-) if [ -z "${DOMAIN}" ]; then RESOLVE_ERROR="Cannot resolve ${HOST}" else debuglog "Adding domain ${DOMAIN} to ${HOST}" HOST="${HOST}.${DOMAIN}" debuglog "New host: ${HOST}" fi fi if [ -z "${RESOLVE_ERROR}" ] ; then # we do not check if localhost can be resolved since on some macOS installations # host localhost will issue an error (Host localhost not found: 3(NXDOMAIN)) if ! echo "${HOST}" | grep -q ':' && echo "${HOST}" | grep -q '[a-z]' && [ "${HOST}" != 'localhost' ] ; then # we have an host name and not an IP address debuglog "Checking if the host (${HOST}) exists" if [ "${INETPROTO}" = '-4' ] ; then if ! host -t a "${HOST}" | grep -q 'has address' ; then RESOLVE_ERROR="Cannot resolve ${HOST} (no A record)" fi elif [ "${INETPROTO}" = '-6' ] ; then if ! host -t aaaa "${HOST}" | grep -q 'has IPv6 address' ; then RESOLVE_ERROR="Cannot resolve ${HOST} (no AAAA record)" fi else if ! host "${HOST}" | grep -q 'has .*address' ; then RESOLVE_ERROR="Cannot resolve ${HOST}" fi fi fi fi if [ -n "${RESOLVE_ERROR}" ] ; then debuglog "${RESOLVE_ERROR}" critical "${SHORTNAME} CRITICAL: ${RESOLVE_ERROR}" fi fi else # from https://superuser.com/questions/1400035/how-to-do-nslookup-or-dns-resolution-using-http-proxy # How to do NSLOOKUP or DNS-resolution using HTTP-PROXY? debuglog "Resolving using DNS over HTTP" create_temporary_file DNS_OVER_HTTP=${TEMPFILE} TIMEOUT_REASON="Resolving over HTTP with ${RESOLVE_OVER_HTTP}" exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --user-agent '${HTTP_USER_AGENT}' -H 'Content-Type: application/dns-json' https://${RESOLVE_OVER_HTTP}/resolve?name=${HOST}\\&type=A" "${DNS_OVER_HTTP}" if [ "${DEBUG}" -ge 1 ]; then jq < "${DNS_OVER_HTTP}" | sed 's/^/[DBG] /' 1>&2 fi if grep -q '"Status":0' "${DNS_OVER_HTTP}" ; then debuglog "Reolved via HTTP" else critical "${SHORTNAME} CRITICAL: Cannot resolve ${HOST} over HTTP using ${RESOLVE_OVER_HTTP}" fi fi else debuglog "Skipping the check to see if the host can be resolved" fi ############################################################################## # End of the "resolve" check # we do quick check if the argument seems an IPv6 address (no validity check) if echo "${HOST}" | "${GREP_BIN}" -q "^[0-9a-fA-F:]*$"; then debuglog "${HOST} seems an IPv6 address without []" HOST="[${HOST}]" fi debuglog "HOST = ${HOST}" info Host "${HOST}" if [ -r "${HOST_CACHE}" ]; then debuglog "Host cache ${HOST_CACHE} is present" if echo "${HOST}" | "${GREP_BIN}" -q -F '['; then PATTERN=$(echo "${HOST}" | sed -e 's/\[//' -e 's/\]//') else PATTERN="^${HOST}$" fi if ! "${GREP_BIN}" -q "${PATTERN}" "${HOST_CACHE}"; then debuglog "Adding ${HOST} to the host cache" echo "${HOST}" >>"${HOST_CACHE}" else debuglog "${HOST} is already cached" fi fi ################################################################################ # Usually SERVERADDR and SERVERNAME both contain the fully qualified domain name # (FQDN) or IP address of the host to check # # If --resolve is specified (defining an alternative IP address for the HOST # we set SERVERADDR to the address specified with --resolve and SERVERNAME to the # FQDN of the host. # # In addition we set the Server Name Indication (SNI) to HOST so that when # connecting with the IP address the server will be able to deliver the # correct certificate # if [ -n "${RESOLVE}" ]; then debuglog "Forcing ${HOST} to resolve to ${RESOLVE}" if echo "${RESOLVE}" | "${GREP_BIN}" -q '^[a-fA-F0-9].*:'; then debuglog "--resolve with an IPv6 (${RESOLVE}) without brackets: adding ([${RESOLVE}])" RESOLVE="[${RESOLVE}]" fi HOST_ADDR="${RESOLVE}" HOST_NAME="${HOST}" SNI="${HOST}" else HOST_ADDR="${HOST}" HOST_NAME="${HOST}" fi debuglog "SNI = ${SNI}" debuglog "HOST_NAME = ${HOST_NAME}" debuglog "HOST_ADDR = ${HOST_ADDR}" debuglog "NAMES_TO_BE_CHECKED = ${NAMES_TO_BE_CHECKED}" # if the host name contains a / (e.g., a URL) the regex for NAMES_TO_BE_CHECKED substitution fails # we check that only allowed characters are present # we don't need a complete validation since a wrong host name will fail anyway HOST_IS_IP="$(is_ip "${HOST_NAME}")" debuglog "HOST_IS_IP. = ${HOST_IS_IP}" if [ "${HOST_IS_IP}" -eq 0 ]; then if ! echo "${HOST_NAME}" | "${GREP_BIN}" -q '^[.a-zA-Z0-9\_\-]*$'; then unknown "Invalid host name: ${HOST_NAME}" fi fi # we accept underscores since some hosts with an underscore exist but these names # are invalid: we issue a small warning if echo "${HOST_NAME}" | "${GREP_BIN}" -q '[\_]' && [ -n "${VERBOSE}" ]; then verboselog "Warning: ${HOST_NAME} contains an underscore (invalid)" fi ################################################################################ # Set NAMES_TO_BE_CHECKED to hostname (replance __HOST__ if present) # NAMES_TO_BE_CHECKED is a space separated list of hostnames. case ${NAMES_TO_BE_CHECKED} in *__HOST__*) # localhost is used for files to be checked: we ignore it IS_IP_TMP="$(is_ip "${HOST_NAME}")" if [ "${HOST_NAME}" != 'localhost' ] && [ "${IS_IP_TMP}" -eq 0 ]; then debuglog "Adding ${HOST_NAME} to NAMES_TO_BE_CHECKED" NAMES_TO_BE_CHECKED=$(echo "${NAMES_TO_BE_CHECKED}" | sed "s/__HOST__/${HOST_NAME}/") else debuglog "Removing __HOST__ to the names to be checked as the host is 'localhost' or an IP address" NAMES_TO_BE_CHECKED=$(echo "${NAMES_TO_BE_CHECKED}" | sed "s/__HOST__//") fi ;; *) ;; esac debuglog "NAMES_TO_BE_CHECKED = ${NAMES_TO_BE_CHECKED}" if [ -n "${ALTNAMES}" ] && [ -z "${IGNORE_HOST_CN}" ] && [ -z "${NAMES_TO_BE_CHECKED}" ] && [ "${HOST_IS_IP}" -eq 0 ]; then unknown "--altnames requires a common name to match (--cn or --host-cn)" fi ############################################################################## # file if [ -z "${FILE_BIN}" ]; then FILE_BIN='file' fi check_required_prog "${FILE_BIN}" FILE_BIN=${PROG} ############################################################################## # Python is needed for TDS if [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" = 'tds' ]; then if [ -z "${PYTHON_BIN}" ]; then PYTHON_BIN='python3' fi check_required_prog "${PYTHON_BIN}" PYTHON_BIN="${PROG}" # check Python major version if "${PYTHON_BIN}" --version 2>&1 | "${GREP_BIN}" -q '^Python 2'; then unknown "Python 2 is not supported" fi fi ############################################################################## # Root certificate if [ -n "${ROOT_CA}" ]; then if [ ! -r "${ROOT_CA}" ]; then critical "Cannot read root certificate ${ROOT_CA}" fi if [ -d "${ROOT_CA}" ]; then ROOT_CA="-CApath ${ROOT_CA}" elif [ -f "${ROOT_CA}" ]; then # check if the file is in DER format and has to be converted if "${FILE_BIN}" -L -b "${ROOT_CA}" | "${GREP_BIN}" -E -q '(data|Certificate)'; then create_temporary_file ROOT_CA_PEM=${TEMPFILE} debuglog "Converting ${ROOT_CA} (DER) to PEM: ${ROOT_CA_PEM}" create_temporary_file CONVERT_ERROR=${TEMPFILE} if ! ${OPENSSL} x509 -inform DER -outform PEM -in "${ROOT_CA}" -out "${ROOT_CA_PEM}" 2>"${CONVERT_ERROR}"; then CONVERT_ERROR=$(head -n 1 "${CONVERT_ERROR}") prepend_critical_message "Error converting ${ROOT_CA} to PEM: ${CONVERT_ERROR}" critical "${SHORTNAME} CRITICAL: Error converting ${ROOT_CA} to PEM: ${CONVERT_ERROR}" fi ROOT_CA="-CAfile ${ROOT_CA_PEM}" else ROOT_CA="-CAfile ${ROOT_CA}" fi else FILE_TMP="$(file "${ROOT_CA}" 2>/dev/null)" critical "Root certificate of unknown type ${FILE_TMP}" fi debuglog "Root CA option = ${ROOT_CA}" fi if [ -n "${REQUIRE_CLIENT_CERT}" ]; then debuglog "Check if at at least one client certificate is accepted" if [ -n "${REQUIRE_CLIENT_CERT_CAS}" ]; then debuglog " from the following CAs: ${REQUIRE_CLIENT_CERT_CAS}" fi fi if [ -n "${ROOT_CA_DIR}" ]; then if [ ! -d "${ROOT_CA_DIR}" ]; then critical "${ROOT_CA_DIR} is not a directory" fi if [ ! -r "${ROOT_CA_DIR}" ]; then critical "Cannot read root directory ${ROOT_CA_DIR}" fi ROOT_CA_DIR="-CApath ${ROOT_CA_DIR}" fi if [ -n "${ROOT_CA_FILE}" ]; then if [ ! -r "${ROOT_CA_FILE}" ]; then critical "Cannot read root certificate ${ROOT_CA_FILE}" fi fi if [ -n "${ROOT_CA_DIR}" ] || [ -n "${ROOT_CA_FILE}" ]; then if [ -n "${ROOT_CA_FILE}" ]; then ROOT_CA="${ROOT_CA_DIR} -CAfile ${ROOT_CA_FILE}" else ROOT_CA="${ROOT_CA_DIR}" fi fi if [ -n "${CLIENT_CERT}" ]; then if [ ! -r "${CLIENT_CERT}" ]; then critical "Cannot read client certificate ${CLIENT_CERT}" fi fi if [ -n "${CLIENT_KEY}" ]; then if [ ! -r "${CLIENT_KEY}" ]; then critical "Cannot read client certificate key ${CLIENT_KEY}" fi fi if [ -n "${FILE}" ]; then # is $FILE a URI? We detect URIs with an "authority" part, since locat paths are specified directly # see https://en.wikipedia.org/wiki/Uniform_Resource_Identifier for the syntax FILE_URI=$(echo "${FILE}" | "${GREP_BIN}" '^[a-z][a-z+.-]*://') debuglog "${FILE} is an URI with an authority" fi if [ -n "${FILE}" ]; then if [ -n "${FILE_URI}" ]; then # we try to fetch it with curl create_temporary_file FILE=${TEMPFILE} debuglog "Fetching ${FILE_URI} to ${FILE}" TIMEOUT_REASON="fetching certificate file" if [ -n "${HTTP_USER_AGENT}" ]; then exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --user-agent '${HTTP_USER_AGENT}' --location \\\"${FILE_URI}\\\" > ${FILE}" else exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent --location \\\"${FILE_URI}\\\" > ${FILE}" fi unset TIMEOUT_REASON if [ ! -r "${FILE}" ]; then critical "Cannot fetch ${FILE_URI}" fi elif [ ! -r "${FILE}" ]; then critical "Cannot read file ${FILE}" elif [ -d "${FILE}" ]; then critical "${FILE} is a directory" elif ! [ -f "${FILE}" ]; then critical "${FILE} is not a regular file" fi fi if [ -n "${CRITICAL_DAYS}" ]; then debuglog "-c specified: ${CRITICAL_DAYS}" if ! echo "${CRITICAL_DAYS}" | "${GREP_BIN}" -E -q '^[0-9][0-9]*(\.[0-9][0-9]*)?$'; then unknown "invalid number of days '${CRITICAL_DAYS}'" fi if echo "${CRITICAL_DAYS}" | "${GREP_BIN}" -q '[.]' && [ -z "${SCALE}" ]; then # floating point critical and no precision set SCALE=2 fi fi if [ -n "${WARNING_DAYS}" ]; then debuglog "-w specified: ${WARNING_DAYS}" if ! echo "${WARNING_DAYS}" | "${GREP_BIN}" -E -q '^[0-9][0-9]*(\.[0-9][0-9]*)?$'; then unknown "invalid number of days '${WARNING_DAYS}'" fi if echo "${WARNING_DAYS}" | "${GREP_BIN}" -q '[.]' && [ -z "${SCALE}" ]; then SCALE=2 fi fi if [ -z "${SCALE}" ]; then # --precision not specified and no floating point critical or warnings --> integer computations SCALE=0 fi # required utilities check_required_prog 'bc' BCBIN=${PROG} check_required_prog 'host' check_required_prog 'hostname' if [ -n "${CRITICAL_DAYS}" ] && [ -n "${WARNING_DAYS}" ] && [ -n "${CRITICAL_SECONDS}" ] && [ -n "${WARNING_SECONDS}" ]; then # When comparing, always use values in seconds, because values in days might be floating point numbers if compare "${WARNING_SECONDS}" '<' "${CRITICAL_SECONDS}"; then unknown "--warning (${WARNING_DAYS}) is less than --critical (${CRITICAL_DAYS})" fi fi if [ -n "${NOT_VALID_LONGER_THAN}" ]; then debuglog "--not-valid-longer-than specified: ${NOT_VALID_LONGER_THAN}" if ! echo "${NOT_VALID_LONGER_THAN}" | "${GREP_BIN}" -q '^[0-9][0-9]*$'; then unknown "invalid number of days '${NOT_VALID_LONGER_THAN}'" fi fi if [ -n "${CRL}" ] && [ -z "${ROOT_CA_FILE}" ]; then unknown "To be able to check CRL we need the Root Cert. Please specify it with the --rootcert-file option" fi if [ -n "${TMPDIR}" ]; then if [ ! -d "${TMPDIR}" ]; then unknown "${TMPDIR} is not a directory" fi if [ ! -w "${TMPDIR}" ]; then unknown "${TMPDIR} is not writable" fi fi if [ -n "${SSL_LAB_CRIT_ASSESSMENT}" ]; then convert_grade "${SSL_LAB_CRIT_ASSESSMENT}" SSL_LAB_CRIT_ASSESSMENT_NUMERIC="${NUMERIC_SSL_LAB_GRADE}" fi if [ -n "${SSL_LAB_WARN_ASSESTMENT}" ]; then convert_grade "${SSL_LAB_WARN_ASSESTMENT}" SSL_LAB_WARN_ASSESTMENT_NUMERIC="${NUMERIC_SSL_LAB_GRADE}" if [ -n "${SSL_LAB_CRIT_ASSESSMENT}" ]; then if [ "${SSL_LAB_WARN_ASSESTMENT_NUMERIC}" -lt "${SSL_LAB_CRIT_ASSESSMENT_NUMERIC}" ]; then unknown '--check-ssl-labs-warn must be greater than -L|--check-ssl-labs' fi fi fi if [ -n "${CHECK_CIPHERS}" ]; then convert_grade "${CHECK_CIPHERS}" CHECK_CIPHERS_NUMERIC="${NUMERIC_SSL_LAB_GRADE}" fi debuglog "ROOT_CA = ${ROOT_CA}" if [ -n "${IGNORE_CONNECTION_STATE}" ]; then if ! echo "${IGNORE_CONNECTION_STATE}" | "${GREP_BIN}" -q '^[0-3]$'; then unknown "The specified state (${IGNORE_CONNECTION_STATE}) is not valid (must be 0,1,2 or 3)" fi fi ####################### # Check needed programs # Signature algorithms if [ -n "${RSA}" ] && [ -n "${ECDSA}" ]; then unknown 'both --rsa and --ecdsa specified: cannot force both ciphers at the same time' fi # check if -sigalgs is available if [ -n "${RSA}" ] || [ -n "${ECDSA}" ]; then if ! "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -F -- -sigalgs; then unknown '--rsa or --ecdsa specified but OpenSSL does not support the -sigalgs option' fi fi if [ -n "${ECDSA}" ]; then # see https://github.com/matteocorti/check_ssl_cert/issues/164#issuecomment-540623344 SSL_AU="ECDSA+SHA1:ECDSA+SHA224:ECDSA+SHA384:ECDSA+SHA256:ECDSA+SHA512" fi if [ -n "${RSA}" ]; then # check if ciphers with PSS are available if ! "${OPENSSL}" ciphers | "${GREP_BIN}" -q -F 'PSS'; then NO_PSS=1 fi if echo "${SSL_VERSION_DISABLED}" | "${GREP_BIN}" -F -q -- '-no_tls1_3' || [ "${SSL_VERSION}" = '-tls1' ] || [ "${SSL_VERSION}" = '-tls1_1' ] || [ "${SSL_VERSION}" = '-tls1_2' ] || [ -n "${NO_PSS}" ]; then # see https://github.com/matteocorti/check_ssl_cert/issues/164#issuecomment-540623344 # see https://github.com/matteocorti/check_ssl_cert/issues/167 # see https://github.com/matteocorti/check_ssl_cert/issues/446 SSL_AU="RSA+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA224:RSA+SHA1:RSA-PSS+SHA256:RSA-PSS+SHA512:RSA-PSS+SHA384" else # see https://github.com/matteocorti/check_ssl_cert/issues/164#issuecomment-540623344 SSL_AU="RSA-PSS+SHA512:RSA-PSS+SHA384:RSA-PSS+SHA256:RSA+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA224:RSA+SHA1" fi fi if [ -n "${SSL_AU}" ]; then if ! "${OPENSSL}" ciphers "${SSL_AU}" >/dev/null 2>&1; then unknown "OpenSSL does not support cipher '${SSL_AU}'" fi SSL_AU="-sigalgs '${SSL_AU}'" fi # mktemp MKTEMP=$(command -v mktemp 2>/dev/null) if [ -z "${MKTEMP}" ]; then debuglog "mktemp not available" else debuglog "mktemp available: ${MKTEMP}" fi # date if [ -z "${DATEBIN}" ]; then check_required_prog 'date' DATEBIN=${PROG} fi FILE_BIN_VERSION="$("${FILE_BIN}" --version 2>&1)" debuglog "file version: ${FILE_BIN_VERSION}" # nmap if [ -z "${NMAP_BIN}" ]; then debuglog "nmap binary not specified" # we check if nmap is available: if not we continue without connection checks and ciphers NMAP_BIN=$(command -v nmap 2>/dev/null) if [ -z "${NMAP_BIN}" ]; then verboselog "cannot find nmap: disabling connection checks and ciphers checks" debuglog "cannot find nmap: disabling connection checks and ciphers checks" DISABLE_NMAP=1 if [ -n "${IGNORE_CONNECTION_STATE}" ] ; then unknown "--ignore-connection-state requires nmap" fi else if [ ! -x "${NMAP_BIN}" ]; then unknown "${NMAP_BIN} is not executable" fi debuglog "nmap available: ${NMAP_BIN}" fi else # we check if the provided binary actually works check_required_prog "${NMAP_BIN}" fi # nmap does not understand brackets in IPv6 addresses NMAP_HOST_ADDR="${HOST_ADDR}" if echo "${NMAP_HOST_ADDR}" | "${GREP_BIN}" -q '^\['; then NMAP_HOST_ADDR=$(echo "${NMAP_HOST_ADDR}" | sed -e 's/^\[//' -e 's/\]$//') fi if [ -z "${ETC_HOSTS}" ] && [ "${ETC_HOSTS}" != '-4' ] ; then # check if the host has an IPv6 address only (as nmap is not able to resolve without the -6 switch) debuglog "Checking IPs: host ${HOST_ADDR}" if echo "${HOST_ADDR}" | "${GREP_BIN}" -q '[a-z]' && ! host "${HOST_ADDR}" | "${GREP_BIN}" -F -q ' has address '; then debuglog "the host does not have an IPv4 address. Trying nmap with -6 to force IPv6 for an IPv6-only host" NMAP_INETPROTO='-6' fi if echo "${NMAP_HOST_ADDR}" | "${GREP_BIN}" -q ':'; then debuglog "host specified as an IPv6 address: forcing IPv6 with nmap" NMAP_INETPROTO='-6' fi else debuglog "Hosts resolved to an IPv4 address with /etc/hosts" fi PERL="$(command -v perl 2>/dev/null)" if [ -n "${PERL}" ]; then debuglog "perl available: ${PERL}" fi if [ -n "${DATEBIN}" ]; then debuglog "date available: ${DATEBIN}" fi DATETYPE="" if ! "${DATEBIN}" +%s >/dev/null 2>&1; then debuglog "no date binary available" # Perl with Date::Parse (optional) test -x "${PERL}" || PERL="" if [ -z "${PERL}" ]; then verboselog "Warning: Perl not found: disabling date computations" fi if ! ${PERL} -e "use Date::Parse;" >/dev/null 2>&1; then verboselog "Perl module Date::Parse not installed: disabling date computations" PERL="" else verboselog "Perl module Date::Parse installed: enabling date computations" DATETYPE="PERL" fi else debuglog 'checking date version' if "${DATEBIN}" --version 2>&1 | "${GREP_BIN}" -F -q GNU; then DATETYPE='GNU' elif "${DATEBIN}" --version 2>&1 | "${GREP_BIN}" -F -q BusyBox; then DATETYPE='BUSYBOX' else DATETYPE='BSD' if "${DATEBIN}" -f "%b %d %T %Y %Z" '' 2>&1 | "${GREP_BIN}" -q -F 'date: unknown option -- f'; then debuglog "Old BSD date without -f: checking for dconv" DCONV_BIN=$(command -v dconv) if [ -z "${DCONV_BIN}" ]; then unknown "Old version of date without the -f option detected and no dconv installed" else debuglog "dconv detected: ${DCONV_BIN}" DATETYPE='DCONV' fi fi fi debuglog "date computation type: ${DATETYPE}" verboselog "Found ${DATETYPE} date with timestamp support: enabling date computations" 2 fi if [ -n "${FORCE_DCONV_DATE}" ] && [ -n "${FORCE_PERL_DATE}" ]; then unknown "--force-dconv-date and --force-perl-date cannot be specified at the same time" fi if [ -n "${FORCE_PERL_DATE}" ]; then DATETYPE="PERL" fi if [ -n "${FORCE_DCONV_DATE}" ]; then debuglog "Forcing date computations with dconv" DATETYPE="DCONV" check_required_prog dconv DCONV_BIN=${PROG} debuglog "dconv binary: ${DCONV_BIN}" fi if [ "${DEBUG}" -ge 1 ]; then debuglog "OpenSSL binary: ${OPENSSL}" if [ "${DEBUG}" -ge 1 ]; then debuglog "OpenSSL info:" ${OPENSSL} version -a | sed 's/^/[DBG] /' 1>&2 fi OPENSSL_DIR="$(${OPENSSL} version -d | sed -E 's/OPENSSLDIR: "([^"]*)"/\1/')" debuglog "OpenSSL configuration directory: ${OPENSSL_DIR}" DEFAULT_CA=0 if [ -f "${OPENSSL_DIR}"/cert.pem ]; then DEFAULT_CA="$("${GREP_BIN}" -c BEGIN "${OPENSSL_DIR}"/cert.pem)" elif [ -f "${OPENSSL_DIR}"/certs ]; then DEFAULT_CA="$("${GREP_BIN}" -c BEGIN "${OPENSSL_DIR}"/certs)" fi debuglog "${DEFAULT_CA} root certificates installed by default" debuglog "Date computation: ${DATETYPE}" fi ################################################################################ # Check if openssl s_client supports the -servername option # # openssl s_client now has a -help option, so we can use that. # Some older versions support -servername, but not -help # => We supply an invalid command line option to get the help # on standard error for these intermediate versions. # SERVERNAME= if ${OPENSSL} version | grep -q -F 'LibreSSL' && [ "${HOST_IS_IP}" -eq 1 ] ; then verboselog 'LibreSSL does not support IP addresses as "servername": disabling virtual server support' elif ${OPENSSL} version | grep -q -F 'LibreSSL' && echo "${HOST_NAME}" | grep -q '^_' ; then verboselog 'LibreSSL does not support a "servername" value beginning with an underscore: disabling virtual server support' else if ${OPENSSL} s_client -help 2>&1 | "${GREP_BIN}" -F -q -- -servername || ${OPENSSL} s_client not_a_real_option 2>&1 | "${GREP_BIN}" -F -q -- -servername; then if [ -n "${SNI}" ]; then SERVERNAME="-servername ${SNI}" else SERVERNAME="-servername ${HOST_NAME}" fi debuglog "'${OPENSSL} s_client' supports '-servername': using ${SERVERNAME}" else verboselog "'${OPENSSL} s_client' does not support '-servername': disabling virtual server support" fi fi debuglog "SERVERNAME=${SERVERNAME}" ################################################################################ # Check if openssl s_client supports the specified protocol if [ -n "${PROTOCOL}" ] && [ "${PROTOCOL}" = 'sieve' ]; then if ${OPENSSL} s_client "${INETPROTO}" -starttls sieve 2>&1 | "${GREP_BIN}" -F -q 'Value must be one of:' || ${OPENSSL} s_client -starttls sieve 2>&1 | "${GREP_BIN}" -F -q 'error: usage:'; then unknown "OpenSSL does not support the protocol sieve" fi fi if [ -n "${PROXY}" ] && [ -n "${NO_PROXY}" ] ; then unknown "Only one of --proxy or --no_proxy can be specified" fi debuglog "Proxy settings (before):" debuglog " http_proxy = ${http_proxy}" debuglog " https_proxy = ${https_proxy}" debuglog " HTTP_PROXY = ${HTTP_PROXY}" debuglog " HTTPS_PROXY = ${HTTPS_PROXY}" ################################################################################ # If --no-proxy was specified unset the http_proxy variables if [ -n "${NO_PROXY}" ]; then debuglog "Disabling the proxy" unset http_proxy unset https_proxy unset HTTP_PROXY unset HTTPS_PROXY fi ################################################################################ # Check if openssl s_client supports the -name option # S_CLIENT_NAME= if ${OPENSSL} s_client -help 2>&1 | "${GREP_BIN}" -F -q -- -name || ${OPENSSL} s_client not_a_real_option 2>&1 | "${GREP_BIN}" -F -q -- -name; then CURRENT_HOSTNAME=$(hostname) S_CLIENT_NAME="-name ${CURRENT_HOSTNAME}" debuglog "'${OPENSSL} s_client' supports '-name': using ${CURRENT_HOSTNAME}" else verboselog "'${OPENSSL} s_client' does not support '-name'" fi ################################################################################ # Check if openssl s_client supports the -xmpphost option # if ${OPENSSL} s_client -help 2>&1 | "${GREP_BIN}" -F -q -- -xmpphost; then XMPPHOST="-xmpphost ${XMPPHOST:-${HOST_NAME}}" debuglog "'${OPENSSL} s_client' supports '-xmpphost': using ${XMPPHOST}" else if [ -n "${XMPPHOST}" ]; then unknown " s_client' does not support '-xmpphost'" fi XMPPHOST= verboselog "'${OPENSSL} s_client' does not support '-xmpphost': disabling 'to' attribute" fi ################################################################################ # check if openssl s_client supports the SSL TLS version if [ -n "${SSL_VERSION}" ]; then if ! "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -- "${SSL_VERSION}"; then unknown "OpenSSL does not support the ${SSL_VERSION} version" fi fi ################################################################################ # --inetproto validation if [ -n "${INETPROTO}" ]; then # validate the arguments if [ "${INETPROTO}" != "-4" ] && [ "${INETPROTO}" != "-6" ]; then VERSION=$(echo "${INETPROTO}" | awk '{ string=substr($0, 2); print string; }') unknown "Invalid argument '${VERSION}': the value must be 4 or 6" fi # Check if openssl s_client supports the -4 or -6 option if ! "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -- "${INETPROTO}"; then unknown "OpenSSL does not support the ${INETPROTO} option" fi # Check if curl is needed and if it supports the -4 and -6 options if [ -z "${CURL_BIN}" ]; then if [ -n "${SSL_LAB_CRIT_ASSESSMENT}" ] || [ -n "${OCSP}" ]; then if ! "${CURL_BIN}" --help all | "${GREP_BIN}" -F -q -- -6 && [ -n "${INETPROTO}" ]; then unknown "curl does not support the ${INETPROTO} option" fi fi fi # check if IPv6 is available locally if command -v ifconfig >/dev/null; then ifconfig -a | "${GREP_BIN}" -F -q inet6 IPV6_INTERFACE=$? elif command -v ip >/dev/null; then ip addr | "${GREP_BIN}" -F -q inet6 IPV6_INTERFACE=$? else unknown "cannot determine if a network interface has IPv6 configured" fi if [ -n "${INETPROTO}" ] && [ "${INETPROTO}" -eq "-6" ] && [ "${IPV6_INTERFACE}" -ne 0 ]; then unknown "cannot connect using IPv6 as no local interface has IPv6 configured" fi # nmap does not have a -4 switch NMAP_INETPROTO='' if [ -n "${INETPROTO}" ] && [ "${INETPROTO}" = '-6' ]; then NMAP_INETPROTO='-6' fi fi ################################################################################ # Check if s_client supports the no_ssl options for S_CLIENT_OPTION in ${SSL_VERSION_DISABLED}; do require_s_client_option "${S_CLIENT_OPTION}" done ############################################################################## # DNSSEC checks # # see # - https://dnsinstitute.com/documentation/dnssec-guide/ch03s02.html # - https://serverfault.com/questions/154016/querying-and-verifying-dnssec # if [ -n "${REQUIRE_DNSSEC}" ]; then if [ -n "${FILE}" ]; then unknown "--require-dnssec cannot be used with --file" fi # we use dig as delv has several problems on macOS and Fedora if [ -z "${DIG_BIN}" ]; then DIG_BIN='dig' fi check_required_prog "${DIG_BIN}" DIG_BIN=${PROG} # a lot of DNS servers have no support for DNSSEC: we use Google's public DNS debuglog "Checking DNSSEC with ${DIG_BIN} +dnssec ${HOST} @8.8.8.8" DIG_OUTPUT=$(${DIG_BIN} +dnssec "${HOST}" @8.8.8.8) if [ "${DEBUG}" -gt 0 ]; then echo "${DIG_OUTPUT}" | sed 's/^/[DBG] /' 1>&2 fi DNSSEC_ERROR= # check for the presence of the Authenticated Data (ad) flag in the header if ! echo "${DIG_OUTPUT}" | "${GREP_BIN}" ';; flags:' | "${GREP_BIN}" -q 'ad[; ]'; then prepend_critical_message "DNSSEC: the Authenticated Data (ad) flag is not present" DNSSEC_ERROR=1 fi # check the DNSSEC OK (do) flag indicating the recursive server is DNSSEC-aware if ! echo "${DIG_OUTPUT}" | "${GREP_BIN}" ', flags:' | "${GREP_BIN}" -q 'do[; ]'; then prepend_critical_message "DNSSEC: the DNSSEC OK (do) flag indicating the recursive server is DNSSEC-aware is not present" DNSSEC_ERROR=1 fi # check for the presence of an additional resource record of type RRSIG, with the same name as the A record. if ! echo "${DIG_OUTPUT}" | "${GREP_BIN}" -q 'RRSIG'; then prepend_critical_message "DNSSEC: the RRSIG resource record is not present" DNSSEC_ERROR=1 fi if [ -z "${DNSSEC_ERROR}" ]; then verboselog "DNSSEC ok" info "DNSSEC" "ok" else verboselog "DNSSEC not present" info "DNSSEC" "no" fi fi ################################################################################ # define the HTTP request string if [ -n "${SNI}" ]; then HOST_HEADER="${SNI}" else HOST_HEADER="${HOST_NAME}" fi debuglog "HOST_HEADER = ${HOST_HEADER}" # add newline if custom HTTP header is defined if [ -n "${CUSTOM_HTTP_HEADER}" ]; then CUSTOM_HTTP_HEADER="${CUSTOM_HTTP_HEADER}\\n" fi # HTTP version if [ "${PROTOCOL}" = 'h2' ]; then HTTP_VERSION="2" else HTTP_VERSION="1.1" fi if [ -n "${IGNORE_MAXIMUM_VALIDITY}" ] && [ -n "${MAXIMUM_VALIDITY}" ]; then unknown "--ignore-maximum-validity and --maximum-validity cannot be specified at the same time" fi if [ -n "${MAXIMUM_VALIDITY}" ] && ! echo "${MAXIMUM_VALIDITY}" | "${GREP_BIN}" -E -q '^[0-9][0-9]*$'; then unknown "invalid number of days '${MAXIMUM_VALIDITY}'" fi # end of sanity checks HTTP_REQUEST="${HTTP_METHOD} ${HTTP_REQUEST_URL} HTTP/${HTTP_VERSION}\\nHost: ${HOST_HEADER}\\nUser-Agent: ${HTTP_USER_AGENT}\\n${CUSTOM_HTTP_HEADER}Connection: close\\n\\n" ############################################################################## # Check for disallowed protocols if [ -n "${DISALLOWED_PROTOCOLS}" ]; then if [ -n "${DISABLE_NMAP}" ]; then if [ -n "${USING_A_PROXY}" ] ; then verboselog "Using a proxy: cannot check for disable protocols" debuglog "Using a proxy: cannot check for disable protocols" fi else # see https://github.com/matteocorti/check_ssl_cert/issues/378 if [ -n "${SNI}" ]; then # https://github.com/matteocorti/check_ssl_cert/issues/505 debuglog "Executing ${NMAP_BIN} -Pn -p \"${PORT}\" \"${NMAP_INETPROTO}\" --script +ssl-enum-ciphers --script-args=tls.servername=\"${SNI}\" \"${HOST_ADDR}\" 2>&1 | grep '^|'" OFFERED_PROTOCOLS=$(${NMAP_BIN} -Pn -p "${PORT}" "${NMAP_INETPROTO}" --script +ssl-enum-ciphers --script-args=tls.servername="${SNI}" "${HOST_ADDR}" 2>&1 | grep '^|') else debuglog "Executing ${NMAP_BIN} -Pn -p \"${PORT}\" \"${NMAP_INETPROTO}\" --script +ssl-enum-ciphers \"${HOST_ADDR}\" 2>&1 | grep '^|'" OFFERED_PROTOCOLS=$(${NMAP_BIN} -Pn -p "${PORT}" "${NMAP_INETPROTO}" --script +ssl-enum-ciphers "${HOST_ADDR}" 2>&1 | grep '^|') fi debuglog "offered ciphers and protocols:" debuglog "${OFFERED_PROTOCOLS}" | sed 's/^|/[DBG] /' DISALLOWED_PROTOCOLS_FAIL= for protocol in ${DISALLOWED_PROTOCOLS}; do debuglog "Checking if '${protocol}' is offered" if echo "${OFFERED_PROTOCOLS}" | "${GREP_BIN}" -F -v 'No supported ciphers found' | "${GREP_BIN}" -q "${protocol}"; then debuglog "'${protocol}' is offered" DISALLOWED_PROTOCOLS_FAIL=1 prepend_critical_message "${protocol} is offered" fi done if [ -z "${DISALLOWED_PROTOCOLS_FAIL}" ]; then verboselog "no disallowed protocols offered" fi fi fi ############################################################################## # DANE if [ -n "${DANE}" ]; then debuglog 'checking DANE' if [ -z "${DIG_BIN}" ]; then DIG_BIN='dig' fi check_required_prog "${DIG_BIN}" DIG_BIN=${PROG} # check if OpenSSL supports -dane_tlsa_rrdata if ${OPENSSL} s_client -help 2>&1 | "${GREP_BIN}" -F -q -- -dane_tlsa_rrdata || ${OPENSSL} s_client not_a_real_option 2>&1 | "${GREP_BIN}" -F -q -- -dane_tlsa_rrdata; then DIG_RESULT=$("${DIG_BIN}" +short TLSA "_${PORT}._tcp.${HOST_ADDR}" | while read -r L; do echo " -dane_tlsa_rrdata '${L}' "; done) debuglog "Checking DANE (${DANE})" debuglog "$(printf '%s\n' "${DIG_BIN} +short TLSA _${PORT}._tcp.${HOST_ADDR} =")" debuglog "${DIG_RESULT}" case ${DANE} in 1) DANE=$(echo "${DIG_RESULT}" | tr -d '\n') ;; 211) DANE=$(echo "${DIG_RESULT}" | "${GREP_BIN}" -F '2 1 1' | tr -d '\n') ;; 301) DANE=$(echo "${DIG_RESULT}" | "${GREP_BIN}" -F '3 0 1' | tr -d '\n') ;; 311) DANE=$(echo "${DIG_RESULT}" | "${GREP_BIN}" -F '3 1 1' | tr -d '\n') ;; 312) DANE=$(echo "${DIG_RESULT}" | "${GREP_BIN}" -F '3 1 2' | tr -d '\n') ;; 302) DANE=$(echo "${DIG_RESULT}" | "${GREP_BIN}" -F '3 0 2' | tr -d '\n') ;; *) unknown "Internal error: unknown DANE check type ${DANE}" ;; esac debuglog "${#DANE} DANE =" debuglog "${DANE}" if [ ${#DANE} -lt 5 ]; then prepend_critical_message "No matching TLSA records found at _${PORT}._tcp.${HOST_ADDR}" critical "${SHORTNAME} CRITICAL: No matching TLSA records found at _${PORT}._tcp.${HOST_ADDR}" else verboselog "DANE OK" fi DANE="${DANE} -dane_tlsa_domain ${HOST_ADDR} " debuglog "DANE = ${DANE}" else unknown 'OpenSSL s_client does not support DNS-based Authentication of Named Entities' fi fi # OpenSSL 3.0.0 gives an error for legacy renegotiation: ignore the error if --ignore-tls-renegotiation was specified if [ -n "${IGNORE_TLS_RENEGOTIATION}" ]; then debuglog "--ignore-tls-renegotiation specified: checking OpenSSL version and -legacy_renegotiation support" if "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -F -- "-legacy_renegotiation"; then debuglog "OpenSSL s_client supports the -legacy_renegotiation option" RENEGOTIATION="-legacy_renegotiation" fi fi if [ -n "${IGNORE_UNEXPECTED_EOF}" ]; then debuglog "--ignore-unexpected-eof specified: checking OpenSSL version and -ignore_unexpected_eof support" if "${OPENSSL}" s_client -help 2>&1 | "${GREP_BIN}" -q -F -- "-ignore_unexpected_eof"; then debuglog "OpenSSL s_client supports the -ignore_unexpected_eof option" IGNOREEOF="-ignore_unexpected_eof" fi fi ################################################################################ # Connection check if [ -z "${FILE}" ]; then if [ -n "${DISABLE_NMAP}" ]; then if [ -n "${USING_A_PROXY}" ] ; then verboselog "Using a proxy: cannot test connection" debuglog "Using a proxy: cannot test connection" fi else debuglog "Testing connection with ${HOST}:${PORT}" debuglog "Executing: '${NMAP_BIN} ${NMAP_INETPROTO} --unprivileged -Pn -p ${PORT} ${NMAP_HOST_ADDR}'" NMAP_OUTPUT=$( ${NMAP_BIN} "${NMAP_INETPROTO}" --unprivileged -Pn -p "${PORT}" "${NMAP_HOST_ADDR}" 2>&1 ) if [ "${DEBUG}" -ge 1 ]; then echo "${NMAP_OUTPUT}" | sed -e 's/^/[DBG] /' fi debuglog "${GREP_BIN} -q \"${PORT}.*open\"" if [ "${DEBUG}" -ge 1 ]; then echo "${NMAP_OUTPUT}" | "${GREP_BIN}" -q "${PORT}.*open" | sed -e 's/^/[DBG] /' fi if ! echo "${NMAP_OUTPUT}" | "${GREP_BIN}" -q "${PORT}.*open"; then if [ -n "${IGNORE_CONNECTION_STATE}" ]; then case "${IGNORE_CONNECTION_STATE}" in "${STATUS_OK}") echo "${SHORTNAME} OK: Cannot connect to ${HOST}:${PORT}" exit "${STATUS_OK}" ;; "${STATUS_WARNING}") echo "${SHORTNAME} WARNING: Cannot connect to ${HOST}:${PORT}" exit "${STATUS_WARNING}" ;; "${STATUS_CRITICAL}") echo "${SHORTNAME} CRITICAL: Cannot connect to ${HOST}:${PORT}" exit "${STATUS_CRITICAL}" ;; "${STATUS_UNKNOWN}") critical "Cannot connect to ${HOST}:${PORT}" ;; *) debuglog "Ignoring connection test" ;; esac else critical "Cannot connect to ${HOST} on port ${PORT}" fi fi fi fi debuglog "Sanity checks: OK" ################################################################################ # Fetch the X.509 certificate # Temporary storage for the certificate and the errors create_temporary_file CERT=${TEMPFILE} create_temporary_file ERROR=${TEMPFILE} create_temporary_file CRL_TMP=${TEMPFILE} create_temporary_file CRL_TMP_PEM=${TEMPFILE} create_temporary_file CRL_TMP_CHAIN=${TEMPFILE} if [ -n "${OCSP}" ]; then create_temporary_file ISSUER_CERT_TMP=${TEMPFILE} create_temporary_file ISSUER_CERT_TMP2=${TEMPFILE} fi if [ -n "${REQUIRE_OCSP_STAPLING}" ]; then create_temporary_file OCSP_RESPONSE_TMP=${TEMPFILE} fi debuglog "Temporary files created" if [ -z "${FILE}" ]; then verboselog "Downloading certificate to ${TMPDIR}" 2 fi CLIENT="" if [ -n "${CLIENT_CERT}" ]; then CLIENT="-cert ${CLIENT_CERT}" fi if [ -n "${CLIENT_KEY}" ]; then CLIENT="${CLIENT} -key ${CLIENT_KEY}" fi CLIENTPASS="" if [ -n "${CLIENT_PASS}" ]; then CLIENTPASS="-pass pass:${CLIENT_PASS}" fi # Cleanup before program termination # Using named signals to be POSIX compliant # shellcheck disable=SC2086 trap_with_arg cleanup ${SIGNALS} fetch_certificate if ascii_grep 'sslv3[ ]alert[ ]unexpected[ ]message' "${ERROR}"; then if [ -n "${SERVERNAME}" ]; then verboselog "'${OPENSSL} s_client' returned an error: trying without '-servername'" SERVERNAME="" fetch_certificate fi if ascii_grep 'sslv3[ ]alert[ ]unexpected[ ]message' "${ERROR}"; then prepend_critical_message 'cannot fetch certificate: OpenSSL got an unexpected message' fi fi #################### # check HTTP headers if [ -n "${REQUIRED_HTTP_HEADERS}" ]; then debuglog "Checking required HTTP headers: ${REQUIRED_HTTP_HEADERS}" for header in $(echo "${REQUIRED_HTTP_HEADERS}" | tr ',' '\n'); do check_required_http_header "${header}" "${HTTP_HEADERS_PATH}" done fi if [ -n "${UNREQUIRED_HTTP_HEADERS}" ]; then debuglog "Checking unwanted HTTP headers: ${UNREQUIRED_HTTP_HEADERS}" for header in $(echo "${UNREQUIRED_HTTP_HEADERS}" | tr ',' '\n'); do check_unrequired_http_header "${header}" "${HTTP_HEADERS_PATH}" done fi # check for TLS renegotiation if openssl_version '3.0.0'; then debuglog 'Skipping TLS renegotiation check as OpenSSL 3.0.0 enforces it by default' else if [ -z "${IGNORE_TLS_RENEGOTIATION}" ] && [ -z "${FILE}" ]; then debuglog "checking TLS renegotiation" verboselog "checking TLS renegotiation" 2 # see https://www.mcafee.com/blogs/enterprise/tips-securing-ssl-renegotiation/ TIMEOUT_REASON="checking TLS renegotiation" case "${PROTOCOL}" in pop3 | ftp | smtp | irc | ldap | imap | postgres | postgresql | sieve | xmpp | xmpp-server | mysql) exec_with_timeout "printf 'R\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} -crlf -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} -starttls ${PROTOCOL} 2>&1 | ${GREP_BIN} -F -q err" RET=$? ;; *) exec_with_timeout "printf 'R\\n' | ${OPENSSL} s_client ${SECURITY_LEVEL} ${INETPROTO} -crlf -connect ${HOST_ADDR}:${PORT} ${SERVERNAME} ${SCLIENT_PROXY} ${SCLIENT_PROXY_ARGUMENT} 2>&1 | ${GREP_BIN} -F -q err" RET=$? ;; esac unset TIMEOUT_REASON if [ "${RET}" -eq 1 ]; then if ascii_grep '^Secure[ ]Renegotiation[ ]IS[ ]NOT' "${CERT}" && ! ascii_grep 'TLSv1.3' "${CERT}"; then prepend_critical_message 'TLS renegotiation is supported but not secure' else verboselog "TLS renegotiation OK" fi else verboselog "TLS renegotiation OK" fi fi fi # check client certificates if [ -n "${REQUIRE_CLIENT_CERT}" ]; then debuglog "Checking required client cert CAs" if ascii_grep "No client certificate CA names sent" "${CERT}"; then prepend_critical_message "Did not return any client certificate CA names" else for ca in $(echo "${REQUIRE_CLIENT_CERT_CAS}" | tr ',' '\n'); do debuglog " checking ${ca}" if ! "${GREP_BIN}" "${ca}" "${CERT}" | "${GREP_BIN}" -q '^C = ' && ! "${GREP_BIN}" "${ca}" "${CERT}" | "${GREP_BIN}" -q '^\/C='; then prepend_critical_message "${ca} is not listed as an acceptable client certificate CA" fi done fi fi if ascii_grep "BEGIN X509 CRL" "${CERT}"; then # we are dealing with a CRL file OPENSSL_COMMAND="crl" OPENSSL_PARAMS="-nameopt utf8,oneline,-esc_msb" OPENSSL_ENDDATE_OPTION="-nextupdate" else # look if we are dealing with a regular certificate file (x509) if ! ascii_grep "CERTIFICATE" "${CERT}"; then if [ -n "${FILE}" ]; then if [ -r "${FILE}" ]; then if "${OPENSSL}" crl -in "${CERT}" -inform DER | "${GREP_BIN}" -F -q "BEGIN X509 CRL"; then debuglog "File is DER encoded CRL" OPENSSL_COMMAND="crl" OPENSSL_PARAMS="-inform DER -nameopt utf8,oneline,-esc_msb" OPENSSL_ENDDATE_OPTION="-nextupdate" else prepend_critical_message "'${FILE}' is not a valid certificate file" fi else prepend_critical_message "'${FILE}' is not readable" fi else # See # http://stackoverflow.com/questions/1251999/sed-how-can-i-replace-a-newline-n # # - create a branch label via :a # - the N command appends a newline and and the next line of the input # file to the pattern space # - if we are before the last line, branch to the created label $!ba # ($! means not to do it on the last line (as there should be one final newline)) # - finally the substitution replaces every newline with a space on # the pattern space ERROR_MESSAGE="$(sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/; /g' "${ERROR}")" verboselog "error: ${ERROR_MESSAGE}" prepend_critical_message "No certificate returned" critical "${CRITICAL_MSG}" fi else # parameters for regular x509 certificates OPENSSL_COMMAND="x509" OPENSSL_PARAMS="-nameopt utf8,oneline,-esc_msb" OPENSSL_ENDDATE_OPTION="-enddate" fi fi verboselog "Parsing the ${OPENSSL_COMMAND} certificate file" 2 ################################################################################ # Parse the X.509 certificate or crl DATE="$(extract_cert_attribute 'enddate' "${CERT}")" debuglog "Valid until ${DATE}" info "Valid until" "${DATE}" if [ "${OPENSSL_COMMAND}" != 'crl' ]; then START_DATE="$(extract_cert_attribute 'startdate' "${CERT}")" info "Valid from" "${START_DATE}" fi if [ "${OPENSSL_COMMAND}" = "crl" ]; then CN="" SUBJECT="" SERIAL=0 OCSP_URI="" VALID_ATTRIBUTES=",lastupdate,nextupdate,issuer," ISSUERS="$(extract_cert_attribute 'issuer' "${CERT}")" else # we need to remove everything before 'CN = ', to remove an eventual email # supplied with / and additional elements (after ', ') if ! CN="$(extract_cert_attribute 'cn' "${CERT}")"; then if [ -z "${ALTNAMES}" ]; then debuglog "certificate without common name (CN), enabling altername names" verboselog "certificate without common name (CN), enabling altername names" ALTNAMES=1 fi fi SUBJECT="$(extract_cert_attribute 'subject' "${CERT}")" debuglog "SUBJECT = ${SUBJECT}" info "Subject" "${CN}" SERIAL="$(extract_cert_attribute 'serial' "${CERT}")" debuglog "SERIAL = ${SERIAL}" info "Serial Number" "${SERIAL}" X509_VERSION="$(extract_cert_attribute 'version' "${CERT}")" debuglog "X509_VERSION = ${X509_VERSION}" info "X.509 version" "${X509_VERSION}" FINGERPRINT="$(extract_cert_attribute 'fingerprint' "${CERT}")" debuglog "FINGERPRINT = ${FINGERPRINT}" FINGERPRINT_INFO="$(echo "${FINGERPRINT}" | sed 's/Fingerprint=//')" info "Fingerprint" "${FINGERPRINT_INFO}" # only works with -ext if check_x509_option '-ext'; then KEY_USAGE="$(extract_cert_attribute 'keyUsage' "${CERT}")" # info if [ -n "${PURPOSE_CRITICAL}" ]; then debuglog "Certificate purpose is defined as critical" PURPOSE_LABEL="Purpose (critical)" else debuglog "Certificate purpose is not defined as critical" PURPOSE_LABEL="Purpose" if [ -n "${REQUIRE_PURPOSE_CRITICAL}" ]; then prepend_critical_message "Certificate purpose is not defined as critical (as required)" fi fi info "${PURPOSE_LABEL}" "${KEY_USAGE}" # check the certificate purpose if [ -n "${REQUIRE_PURPOSE}" ]; then debuglog "Checking certificate purpose(s)" while IFS= read -r purpose; do debuglog " Check if '${purpose}' is defined" # the purposes are in a 'comma space' separated list if ! echo "${KEY_USAGE}" | "${GREP_BIN}" -q -i "^${purpose}$" && ! echo "${KEY_USAGE}" | "${GREP_BIN}" -q -i "^${purpose}, " && ! echo "${KEY_USAGE}" | "${GREP_BIN}" -q -i ", ${purpose}$" && ! echo "${KEY_USAGE}" | "${GREP_BIN}" -q -i ", ${purpose}, "; then prepend_critical_message "'${purpose}' is not specified as a certificate purpose" fi done <"${OCSP_RESPONSE_TMP}" debuglog "${OCSP_RESPONSE_TMP}" if ! ascii_grep 'Next Update' "${OCSP_RESPONSE_TMP}"; then prepend_critical_message "OCSP stapling not enabled" else NEXT_UPDATE=$("${GREP_BIN}" -o 'Next Update: .*$' "${OCSP_RESPONSE_TMP}" | cut -b14-) hours_until "${NEXT_UPDATE}" OCSP_EXPIRES_IN_HOURS="${HOURS_UNTIL}" verboselog "OCSP stapling expires in ${OCSP_EXPIRES_IN_HOURS} hours" if [ -n "${OCSP_CRITICAL}" ] && compare "${OCSP_CRITICAL}" '>=' "${OCSP_EXPIRES_IN_HOURS}"; then prepend_critical_message "${OPENSSL_COMMAND} OCSP stapling will expire in ${OCSP_EXPIRES_IN_HOURS} hour(s) on ${NEXT_UPDATE}" elif [ -n "${OCSP_WARNING}" ] && compare "${OCSP_WARNING}" '>=' "${OCSP_EXPIRES_IN_HOURS}"; then append_warning_message "${OPENSSL_COMMAND} OCSP stapling will expire in ${OCSP_EXPIRES_IN_HOURS} hour(s) on ${NEXT_UPDATE}" fi fi fi PUB_KEY_ALGORITHM="$(extract_cert_attribute 'pub_key_algo' "${CERT}" | sed 's/.*: //')" info "Public key algorithm" "${PUB_KEY_ALGORITHM}" SIGNATURE_ALGORITHM="$(extract_cert_attribute 'sig_algo' "${CERT}" | sed 's/.*: //')" info "Signature algorithm" "${SIGNATURE_ALGORITHM}" if [ "${DEBUG}" -ge 1 ]; then debuglog "${SUBJECT}" debuglog "CN = ${CN}" # shellcheck disable=SC2162 echo "${ISSUERS}" | while read LINE; do debuglog "CA = ${LINE}" done debuglog "SERIAL = ${SERIAL}" debuglog "FINGERPRINT= ${FINGERPRINT}" debuglog "OCSP_URI = ${OCSP_URI}" debuglog "ISSUER_URI = ${ISSUER_URI}" debuglog "${PUB_KEY_ALGORITHM}" fi if [ -n "${ISSUER_URI}" ]; then info "Issuer URI" "${ISSUER_URI}" else info "No issuer URI" fi # shellcheck disable=SC2116,SC2086 ISSUER_INFO="$(echo ${ISSUERS})" info "Issuers" "${ISSUER_INFO}" if echo "${PUB_KEY_ALGORITHM}" | "${GREP_BIN}" -F -q "sha1"; then if [ -n "${NOSIGALG}" ]; then verboselog "${OPENSSL_COMMAND} Certificate is signed with SHA-1" else prepend_critical_message "${OPENSSL_COMMAND} Certificate is signed with SHA-1" fi fi if echo "${PUB_KEY_ALGORITHM}" | "${GREP_BIN}" -F -qi "md5"; then if [ -n "${NOSIGALG}" ]; then verboselog "${OPENSSL_COMMAND} Certificate is signed with MD5" else prepend_critical_message "${OPENSSL_COMMAND} Certificate is signed with MD5" fi fi ################################################################################ # Generate the long output if [ -n "${LONG_OUTPUT_ATTR}" ]; then check_attr() { ATTR="$1" if ! echo "${VALID_ATTRIBUTES}" | "${GREP_BIN}" -q ",${ATTR},"; then unknown "Invalid certificate attribute: ${ATTR}" else # shellcheck disable=SC2086 value="$(${OPENSSL} "${OPENSSL_COMMAND}" ${OPENSSL_PARAMS} -in "${CERT}" -noout -nameopt utf8,oneline,-esc_msb -"${ATTR}" | sed -e "s/.*=//")" LONG_OUTPUT="${LONG_OUTPUT}\\n${ATTR}: ${value}" fi } # Split on comma if [ "${LONG_OUTPUT_ATTR}" = "all" ]; then LONG_OUTPUT_ATTR="${VALID_ATTRIBUTES}" fi attributes=$(echo "${LONG_OUTPUT_ATTR}" | tr ',' '\n') for attribute in ${attributes}; do check_attr "${attribute}" done LONG_OUTPUT="$(echo "${LONG_OUTPUT}" | sed 's/\\n/\n/g')" fi ################################################################################ # Check the presence of a subjectAlternativeName (required for Chrome) # Do not use grep --after-context=NUM but -A NUM so that it works on BusyBox SUBJECT_ALTERNATIVE_NAME="$(extract_cert_attribute 'subjectAlternativeName' "${CERT}")" debuglog "subjectAlternativeName = ${SUBJECT_ALTERNATIVE_NAME}" if [ -n "${REQUIRE_SAN}" ] && [ -z "${SUBJECT_ALTERNATIVE_NAME}" ] && [ "${OPENSSL_COMMAND}" != "crl" ]; then prepend_critical_message "The certificate for this site does not contain a Subject Alternative Name extension containing a domain name or IP address." else verboselog "The certificate for this site contains a Subject Alternative Name extension" fi for san in ${SUBJECT_ALTERNATIVE_NAME}; do info "Subject Alternative Name" "${san}" done ################################################################################ # Check the names in the certificate if [ -n "${NAMES_TO_BE_CHECKED}" ]; then debuglog "Check the common name and alternative names" # the CN of the certificate (can also be empty) debuglog "CN = ${CN}" # the subject alternative names present in the certificate if [ "${DEBUG}" -gt 0 ]; then echo "${SUBJECT_ALTERNATIVE_NAME}" | tr ' ' '\n' | sed 's/^/[DBG] SUBJECT_ALTERNATIVE_NAME = /' 1>&2 fi # should we check the alternative names (almost always 1) debuglog "ALTNAMES = ${ALTNAMES}" # the names that should be present debuglog "NAMES_TO_BE_CHECKED = ${NAMES_TO_BE_CHECKED}" for name in ${NAMES_TO_BE_CHECKED}; do debuglog " checking '${name}'" ok="" # 1) common name # Common name is case insensitive: using grep for comparison (and not 'case' with 'shopt -s nocasematch' as not defined in POSIX debuglog " common name" if echo "${CN}" | "${GREP_BIN}" -q -i '^\*\.'; then # wildcard # Or the literal with the wildcard CN_TMP="$(echo "${CN}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9_\-]*/')" debuglog " checking (1) if ${name} matches ^${CN_TMP}\$" if echo "${name}" | "${GREP_BIN}" -q -i "^${CN_TMP}\$"; then debuglog " ${name} matches ^${CN_TMP}\$" ok="true" fi # Or if both are exactly the same debuglog " checking (2) if the ${name} matches ^${CN}\$" if echo "${name}" | "${GREP_BIN}" -q -i "^${CN}\$"; then debuglog " ${name} matches ^${CN}\$" ok="true" fi else debuglog " checking if (3) ${name} matches ^${CN}\$" if echo "${name}" | "${GREP_BIN}" -q -i "^${CN}$"; then debuglog " ${name} matches ^${CN}\$" ok="true" fi fi if [ -n "${ALTNAMES}" ]; then debuglog " alternative names" for alt_name in ${SUBJECT_ALTERNATIVE_NAME}; do debuglog " check altname: ${alt_name}" if echo "${alt_name}" | "${GREP_BIN}" -q -i '^\*\.'; then # Match the domain debuglog " the altname ${alt_name} begins with a '*'" ALT_NAME_TMP="$(echo "${alt_name}" | cut -c 3-)" debuglog " checking if (4) ${name} matches ^${ALT_NAME_TMP}\$" if echo "${name}" | "${GREP_BIN}" -q -i "^${ALT_NAME_TMP}\$"; then debuglog " ${name} matches ^${ALT_NAME_TMP}\$" ok="true" fi # Or the literal with the wildcard ALT_NAME_TMP="$(echo "${alt_name}" | sed -e 's/[.]/[.]/g' -e 's/[*]/[A-Za-z0-9_\-]*/')" debuglog " checking if (5) ${name} matches ^${ALT_NAME_TMP}\$" if echo "${name}" | "${GREP_BIN}" -q -i "^${ALT_NAME_TMP}\$"; then debuglog " ${name} matches ^${ALT_NAME_TMP}\$" ok="true" fi # Or if both are exactly the same debuglog " checking if (6) ${name} matches ^${alt_name}\$" if echo "${name}" | "${GREP_BIN}" -q -i "^${alt_name}\$"; then debuglog " ${name} matches ^${alt_name}\$" ok="true" fi else if echo "${name}" | "${GREP_BIN}" -q -i "^${alt_name}$"; then debuglog " ${name} matches ^${alt_name}\$" ok="true" fi fi done if [ -z "${ok}" ]; then prepend_critical_message "'${name}' does not match the CN nor an alternative name" fi else debuglog "Ignoring altnames" if [ -z "${ok}" ]; then prepend_critical_message "'${name}' does not match the CN (altnames ignored)" fi fi done debuglog " CN check finished" fi ################################################################################ # Check the issuer if [ -n "${ISSUER}" ]; then debuglog "check ISSUER: ${ISSUER}" ok="" CA_ISSUER_MATCHED=$(echo "${ISSUERS}" | "${GREP_BIN}" -E "^${ISSUER}\$" | head -n1) debuglog " issuer matched = ${CA_ISSUER_MATCHED}" if [ -n "${CA_ISSUER_MATCHED}" ]; then verboselog "The certificate issuer matches ${ISSUER}" ok="true" else # this looks ugly but preserves spaces in CA name ISSUER_TMP="$(echo "${ISSUER}" | sed "s/|/ PIPE /g")" ISSUERS_TMP="$(echo "${ISSUERS}" | tr '\n' '|' | sed 's/|$//g' | sed "s/|/\\' or \\'/g")" prepend_critical_message "invalid CA ('${ISSUER_TMP}' does not match '${ISSUERS_TMP}')" fi fi ################################################################################ # Check if not issued by if [ -n "${NOT_ISSUED_BY}" ]; then debuglog "check NOT_ISSUED_BY: ${NOT_ISSUED_BY}" debuglog " executing echo \"${ISSUERS}\" | sed -E -e \"s/^(O|CN) ?= ?//\" | ${GREP_BIN} -E \"^${NOT_ISSUED_BY}\$\" | head -n1" ok="" CA_ISSUER_MATCHED=$(echo "${ISSUERS}" | sed -E -e "s/^(O|CN) ?= ?//" | "${GREP_BIN}" -E "^${NOT_ISSUED_BY}\$" | head -n1) debuglog " issuer matched = ${CA_ISSUER_MATCHED}" if [ -n "${CA_ISSUER_MATCHED}" ]; then # this looks ugly but preserves spaces in CA name NOT_ISSUED_BY_TMP="$(echo "${NOT_ISSUED_BY}" | sed "s/|/ PIPE /g")" ISSUERS_TMP="$(echo "${ISSUERS}" | sed -E -e "s/^(O|CN) ?= ?//" | tr '\n' '|' | sed 's/|$//g' | sed "s/|/\\' or \\'/g")" prepend_critical_message "invalid CA ('${NOT_ISSUED_BY_TMP}' matches '${ISSUERS_TMP}')" else ok="true" CA_ISSUER_MATCHED="$(echo "${ISSUERS}" | ${GREP_BIN} -E "^CN ?= ?" | sed -E -e "s/^CN ?= ?//" | head -n1)" fi else CA_ISSUER_MATCHED="$(echo "${ISSUERS}" | head -n1)" fi ################################################################################ # Check the serial number if [ -n "${SERIAL_LOCK}" ]; then ok="" if echo "${SERIAL}" | "${GREP_BIN}" -q "^${SERIAL_LOCK}\$"; then ok="true" fi if [ -z "${ok}" ]; then SERIAL_LOCK_TMP="$(echo "${SERIAL_LOCK}" | sed "s/|/ PIPE /g")" prepend_critical_message "invalid serial number ('${SERIAL_LOCK_TMP}' does not match '${SERIAL}')" else verboselog "Valid serial number (${SERIAL})" fi fi ################################################################################ # Check the Fingerprint if [ -n "${FINGERPRINT_LOCK}" ]; then ok="" if echo "${FINGERPRINT}" | "${GREP_BIN}" -q -E "^${FINGERPRINT_LOCK}\$"; then ok="true" fi if [ -z "${ok}" ]; then FINGERPRINT_LOCK_TMP="$(echo "${FINGERPRINT_LOCK}" | sed "s/|/ PIPE /g")" prepend_critical_message "invalid ${FINGERPRINT_ALG} Fingerprint ('${FINGERPRINT_LOCK_TMP}' does not match '${FINGERPRINT}')" else verboselog "Valid ${FINGERPRINT_ALG} fingerprint (${FINGERPRINT})" fi fi ################################################################################ # Check the validity if [ -z "${NOEXP}" ]; then debuglog "Checking expiration date" if [ -n "${FIRST_ELEMENT_ONLY}" ] || [ "${OPENSSL_COMMAND}" = "crl" ]; then debuglog "Only one element or CRL" DATE_TMP="$(cat "${CERT}")" check_cert_end_date "${DATE_TMP}" else # count the certificates in the chain NUM_CERTIFICATES=$("${GREP_BIN}" -F -c -- "-BEGIN CERTIFICATE-" "${CERT}") debuglog "Number of certificates in CA chain: $((NUM_CERTIFICATES))" # a file could contain more than one certificate for the same CN # if both a valid and an expired certificate for the same CN are present # browsers usually do not complain (see #416) # list of CNs for which a valid certificate was found CN_OK='' CN_EXPIRED_CRITICAL='' CN_EXPIRED_WARNING='' CERT_IN_CHAIN=1 while [ "${CERT_IN_CHAIN}" -le "${NUM_CERTIFICATES}" ]; do debuglog '------------------------------------------------------------------------------' debuglog "-- Checking element ${CERT_IN_CHAIN}" if echo "${SKIP_ELEMENT}" | "${GREP_BIN}" -q "${CERT_IN_CHAIN}"; then debuglog " skipping element ${CERT_IN_CHAIN}" CERT_IN_CHAIN=$((CERT_IN_CHAIN + 1)) continue fi elem_number=$((CERT_IN_CHAIN)) chain_element=$(sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' "${CERT}" | awk -v n="${CERT_IN_CHAIN}" '/-BEGIN CERTIFICATE-/{l++} (l==n) {print}') check_cert_end_date "${chain_element}" "${elem_number}" debuglog '------------------------------------------------------------------------------' check_ocsp "${chain_element}" "${elem_number}" if [ -n "${CRL}" ]; then debuglog '------------------------------------------------------------------------------' check_crl "${chain_element}" "${elem_number}" fi CERT_IN_CHAIN=$((CERT_IN_CHAIN + 1)) if ! [ "${ELEMENT}" -eq 0 ] && [ $((ELEMENT - CERT_IN_CHAIN)) -lt 0 ]; then break fi done debuglog '------------------------------------------------------------------------------' fi # a file could contain more than one certificate for the same CN # if both a valid and an expired certificate for the same CN are present # browsers usually do not complain (see #416) # loop over the criticals if [ -n "${CN_EXPIRED_CRITICAL}" ]; then while IFS= read -r critical; do CN_TMP=$(echo "${critical}" | sed 's/:.*//') REPLACE_CURRENT_MESSAGE=$(echo "${critical}" | sed -e 's/^[^:]*://' -e 's/:.*//') MESSAGE_TMP=$(echo "${critical}" | sed 's/^[^:]*:[^:]*://') # check if the warning is overridden by another certificate for the same CN if echo "${CN_OK}" | "${GREP_BIN}" -q "${CN_TMP}"; then verboselog "Both a valid and an expired certificate were found" if [ -n "${CHECK_CHAIN}" ]; then prepend_critical_message "Both a valid and an expired certificate were found" fi else prepend_critical_message "${MESSAGE_TMP}" "${REPLACE_CURRENT_MESSAGE}" fi done < ${JSON}" CURL_RETURN_CODE=$? else exec_with_timeout "${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${CURL_QUIC} ${INETPROTO} --silent \\\"https://api.ssllabs.com/api/v2/analyze?host=${HOST_NAME}${IGNORE_SSL_LABS_CACHE}\\\" > ${JSON}" CURL_RETURN_CODE=$? fi debuglog "curl return code = ${CURL_RETURN_CODE}" if [ "${CURL_RETURN_CODE}" -ne 0 ]; then if [ -n "${IGNORE_SSL_LABS_ERRORS}" ] ; then break fi if [ "${CURL_RETURN_CODE}" -eq 35 ]; then debuglog "curl returned ${CURL_RETURN_CODE}: ${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${INETPROTO} --silent \"https://api.ssllabs.com/api/v2/analyze?host=${HOST_NAME}${IGNORE_SSL_LABS_CACHE}\"" critical "Error checking SSL Labs: TLS handshake error" else debuglog "curl returned ${CURL_RETURN_CODE}: ${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${INETPROTO} --silent \"https://api.ssllabs.com/api/v2/analyze?host=${HOST_NAME}${IGNORE_SSL_LABS_CACHE}\"" critical "Error checking SSL Labs: curl returned ${CURL_RETURN_CODE}, see 'man curl' for details" fi fi debuglog "Checking SSL Labs: ${CURL_BIN} ${CURL_PROXY} ${CURL_PROXY_ARGUMENT} ${INETPROTO} --silent \"https://api.ssllabs.com/api/v2/analyze?host=${HOST_NAME}\"" if [ "${DEBUG}" -gt 0 ]; then sed 's/^/[DBG] SSL Labs JSON/' "${JSON}" 1>&2 echo 1>&2 fi # We clear the cache only on the first run IGNORE_SSL_LABS_CACHE="" if "${GREP_BIN}" -F -q 'Running[ ]at[ ]full[ ]capacity.[ ]Please[ ]try[ ]again[ ]later' "${JSON}"; then verboselog ' SSL Labs running at full capacity' else SSL_LABS_HOST_STATUS=$(sed 's/.*"status":[ ]*"\([^"]*\)".*/\1/' "${JSON}") debuglog "SSL Labs status: ${SSL_LABS_HOST_STATUS}" case "${SSL_LABS_HOST_STATUS}" in 'ERROR') if [ -n "${IGNORE_SSL_LABS_ERRORS}" ] ; then break fi SSL_LABS_STATUS_MESSAGE=$(sed 's/.*"statusMessage":[ ]*"\([^"]*\)".*/\1/' "${JSON}") prepend_critical_message "Error checking SSL Labs: ${SSL_LABS_STATUS_MESSAGE}" break ;; 'READY') if ! "${GREP_BIN}" -F -q "grade" "${JSON}"; then # Something went wrong if [ -n "${IGNORE_SSL_LABS_ERRORS}" ] ; then break fi SSL_LABS_STATUS_MESSAGE=$(sed 's/.*"statusMessage":[ ]*"\([^"]*\)".*/\1/' "${JSON}") prepend_critical_message "SSL Labs error: ${SSL_LABS_STATUS_MESSAGE}" break else SSL_LABS_HOST_GRADE=$(sed 's/.*"grade":[ ]*"\([^"]*\)".*/\1/' "${JSON}") debuglog "SSL Labs grade: ${SSL_LABS_HOST_GRADE}" verboselog "SSL Labs grade: ${SSL_LABS_HOST_GRADE}" convert_grade "${SSL_LABS_HOST_GRADE}" SSL_LABS_HOST_GRADE_NUMERIC="${NUMERIC_SSL_LAB_GRADE}" add_performance_data "ssllabs=${SSL_LABS_HOST_GRADE_NUMERIC}%;;${SSL_LAB_CRIT_ASSESSMENT_NUMERIC}" # Check the grade if [ "${SSL_LABS_HOST_GRADE_NUMERIC}" -lt "${SSL_LAB_CRIT_ASSESSMENT_NUMERIC}" ]; then prepend_critical_message "SSL Labs grade is ${SSL_LABS_HOST_GRADE} (instead of ${SSL_LAB_CRIT_ASSESSMENT})" elif [ -n "${SSL_LAB_WARN_ASSESTMENT_NUMERIC}" ]; then if [ "${SSL_LABS_HOST_GRADE_NUMERIC}" -lt "${SSL_LAB_WARN_ASSESTMENT_NUMERIC}" ]; then append_warning_message "SSL Labs grade is ${SSL_LABS_HOST_GRADE} (instead of ${SSL_LAB_WARN_ASSESTMENT})" fi fi debuglog "SSL Labs grade (converted): ${SSL_LABS_HOST_GRADE_NUMERIC}" # We have a result: exit break fi ;; 'IN_PROGRESS') # Data not yet available: warn and continue PROGRESS=$(sed 's/.*progress"://' "${JSON}" | sed 's/,.*//') debuglog "Progress = ${PROGRESS}" if [ "${PROGRESS}" -eq -1 ]; then verboselog " warning: no cached data by SSL Labs, check in progress" 2 else verboselog " warning: no cached data by SSL Labs, check in progress ${PROGRESS}%" 2 fi ;; 'DNS') verboselog " SSL Labs resolving the domain name" 2 ;; *) # Try to extract a message if [ -n "${IGNORE_SSL_LABS_ERRORS}" ] ; then break fi SSL_LABS_ERROR_MESSAGE=$(sed 's/.*"message":[ ]*"\([^"]*\)".*/\1/' "${JSON}") if [ -z "${SSL_LABS_ERROR_MESSAGE}" ]; then SSL_LABS_ERROR_MESSAGE="cat ${JSON}" fi prepend_critical_message "Cannot check status on SSL Labs: ${SSL_LABS_ERROR_MESSAGE}" ;; esac fi WAIT_TIME=60 verboselog " waiting ${WAIT_TIME} seconds" 2 exec_with_timeout "sleep ${WAIT_TIME}" done unset TIMEOUT_REASON fi ################################################################################ # Check the organization if [ -n "${ORGANIZATION}" ]; then debuglog "Checking organization ${ORGANIZATION}" ORG="$(extract_cert_attribute 'org' "${CERT}")" debuglog " ORG = ${ORG}" debuglog " ORGANIZATION = ${ORGANIZATION}" if ! echo "${ORG}" | "${GREP_BIN}" -q -E "^${ORGANIZATION}"; then ORGANIZATION_TMP="$(echo "${ORGANIZATION}" | sed "s/|/ PIPE /g")" prepend_critical_message "invalid organization ('${ORGANIZATION_TMP}' does not match '${ORG}')" fi fi if [ "${OPENSSL_COMMAND}" != 'crl' ]; then EMAIL="$(extract_cert_attribute 'email' "${CERT}")" debuglog "EMAIL = ${EMAIL}" info "Email" "${EMAIL}" fi if [ -n "${INFO}" ]; then # see https://stackoverflow.com/questions/6464129/certificate-subject-x-509 for additional fields that could be implemented CERT_ORG="$(extract_cert_attribute 'org' "${CERT}")" info "Organization" "${CERT_ORG}" CERT_OU="$(extract_cert_attribute 'org_unit' "${CERT}")" info "Organizational unit" "${CERT_OU}" CERT_COUNTRY="$(extract_cert_attribute 'country' "${CERT}")" info "Country" "${CERT_COUNTRY}" CERT_STATE="$(extract_cert_attribute 'state' "${CERT}")" info "State or province" "${CERT_STATE}" CERT_LOCALITY="$(extract_cert_attribute 'locality' "${CERT}")" info "Locality" "${CERT_LOCALITY}" KEY_LENGTH="$(extract_cert_attribute 'key_length' "${CERT}")" info "Public key length" "${KEY_LENGTH}" fi ################################################################################ # Check the email if [ -n "${ADDR}" ]; then if [ -z "${EMAIL}" ]; then debuglog "no email in certificate" prepend_critical_message "the certificate does not contain an email address" else if ! echo "${EMAIL}" | "${GREP_BIN}" -q -E "^${ADDR}"; then EMAIL_TMP="$(echo "${ADDR}" | sed "s/|/ PIPE /g")" prepend_critical_message "invalid email ('${EMAIL_TMP}' does not match ${EMAIL})" else verboselog "email ${ADDR} is OK" fi fi fi ################################################################################ # Check if the certificate was verified if [ -z "${NOAUTH}" ] && ascii_grep '^verify[ ]error:' "${ERROR}"; then debuglog 'Checking if the certificate was self signed' if ascii_grep '^verify[ ]error:num=[0-9][0-9]*:self[ -]signed[ ]certificate' "${ERROR}"; then debuglog 'Self signed certificate' if [ -z "${SELFSIGNED}" ]; then prepend_critical_message "Cannot verify certificate, self signed certificate" else SELFSIGNEDCERT="self signed " fi elif ascii_grep '^verify[ ]error:num=[0-9][0-9]*:certificate[ ]has[ ]expired' "${ERROR}"; then debuglog 'Cannot verify since the certificate has expired.' elif ascii_grep '^verify[ ]error:num=[0-9][0-9]*:unable to get local issuer certificate' "${ERROR}"; then if [ -z "${IGNORE_INCOMPLETE_CHAIN}" ]; then prepend_critical_message "Cannot verify certificate, cannot verify certificate chain (local issuer certificate)" fi else DEBUG_MESSAGE="$(sed 's/^/Error: /' "${ERROR}")" debuglog "${DEBUG_MESSAGE}" # Process errors details=$("${GREP_BIN}" '^verify error:' "${ERROR}" | sed 's/verify error:num=[0-9]*://') prepend_critical_message "Cannot verify certificate: ${details}" fi else verboselog "The certificate was successfully verified" fi ############################################################################## # Check for Signed Certificate Timestamps (SCT) if [ -z "${SELFSIGNED}" ] && [ "${OPENSSL_COMMAND}" != "crl" ]; then # check if OpenSSL supports SCTs if openssl_version '1.1.0'; then debuglog 'Checking Signed Certificate Timestamps (SCTs)' if [ -n "${SCT}" ] && ! extract_cert_attribute 'sct' "${CERT}"; then prepend_critical_message "Cannot find Signed Certificate Timestamps (SCT)" else info "SCT" "yes" verboselog "The certificate contains signed certificate timestamps (SCT)" fi else verboselog 'warning: Skipping SCTs check as not supported by OpenSSL' fi fi ############################################################################## # Check total certificate validity if [ -z "${IGNORE_MAXIMUM_VALIDITY}" ]; then # we check only for HTTP protocols, files or if --maximum-validity was specified if [ -z "${PROTOCOL}" ] || [ "${PROTOCOL}" = 'https' ] || [ "${PROTOCOL}" = 'h2' ] || [ -n "${MAXIMUM_VALIDITY}" ] || [ -n "${FILE}" ]; then hours_until "${DATE}" HOURS_UNTIL_END_DATE="${HOURS_UNTIL}" debuglog "Total certificate validity: ${HOURS_UNTIL} hours until ${DATE}" hours_until "${START_DATE}" HOURS_FROM_START_DATE="${HOURS_UNTIL}" debuglog "Total certificate validity: ${HOURS_UNTIL} hours until ${START_DATE}" # no decimals even if --precision was specified TOTAL_CERT_VALIDITY=$(compute "(${HOURS_UNTIL_END_DATE} - ${HOURS_FROM_START_DATE})/24" 0) debuglog "Total certificate validity in days: ${TOTAL_CERT_VALIDITY}" LIMIT=397 if [ -n "${MAXIMUM_VALIDITY}" ]; then LIMIT="${MAXIMUM_VALIDITY}" fi # a certificate cannot be valid for more than 13 months (397 days) if [ "${TOTAL_CERT_VALIDITY}" -gt "${LIMIT}" ]; then prepend_critical_message "The certificate cannot be valid for more than ${LIMIT} days (${TOTAL_CERT_VALIDITY})" else verboselog "The certificate validity (${TOTAL_CERT_VALIDITY}) is shorter then the maximum (${LIMIT})" fi else verboselog "Skipping maximum validity test for non HTTP protocols" fi fi ############################################################################## # Criticals and warnings # if errors exist at this point return if [ -n "${CRITICAL_MSG}" ]; then critical "${CRITICAL_MSG}" fi if [ -n "${WARNING_MSG}" ]; then warning "${WARNING_MSG}" fi ################################################################################ # If we get this far, assume all is well. :) if [ -z "${QUIET}" ]; then # If --altnames was specified or if the certificate is wildcard, # then we show the specified CN in addition to the certificate CN CHECKEDNAMES="" if [ -n "${ALTNAMES}" ] && [ -n "${NAMES_TO_BE_CHECKED}" ] && [ "${CN}" != "${NAMES_TO_BE_CHECKED}" ]; then CHECKEDNAMES="(${NAMES_TO_BE_CHECKED}) " elif [ -n "${NAMES_TO_BE_CHECKED}" ] && echo "${CN}" | "${GREP_BIN}" -q -i '^\*\.'; then CHECKEDNAMES="(${NAMES_TO_BE_CHECKED}) " fi if [ -n "${DAYS_VALID}" ]; then # nicer formatting if compare "${DAYS_VALID}" '>=' 1; then DAYS_VALID=" (expires in ${DAYS_VALID} days)" elif compare "${DAYS_VALID}" '>=' 0; then DAYS_VALID=" (expires in less than a day)" elif compare "${DAYS_VALID}" '>=' '-1'; then DAYS_VALID=$((-DAYS_VALID)) DAYS_VALID=" (expired ${DAYS_VALID} days ago)" fi fi if [ -n "${OCSP_EXPIRES_IN_HOURS}" ]; then # nicer formatting if compare "${OCSP_EXPIRES_IN_HOURS}" '>=' 2; then OCSP_EXPIRES_IN_HOURS=" (OCSP stapling expires in ${OCSP_EXPIRES_IN_HOURS} hours)" elif compare "${OCSP_EXPIRES_IN_HOURS}" '>=' 1; then OCSP_EXPIRES_IN_HOURS=" (OCSP stapling expires in one hour)" elif compare "${OCSP_EXPIRES_IN_HOURS}" '>=' 0; then OCSP_EXPIRES_IN_HOURS=" (OCSP stapling expires now)" elif compare "${OCSP_EXPIRES_IN_HOURS}" '>=' '-1'; then OCSP_EXPIRES_IN_HOURS=" (OCSP stapling expired one hour ago)" else OCSP_EXPIRES_IN_HOURS=" (OCSP stapling expired ${OCSP_EXPIRES_IN_HOURS} hours ago)" fi fi if [ -n "${SSL_LABS_HOST_GRADE}" ]; then SSL_LABS_HOST_GRADE=", SSL Labs grade: ${SSL_LABS_HOST_GRADE}" fi if [ -z "${CN}" ]; then DISPLAY_CN="" else DISPLAY_CN="'${CN}' " fi if [ -z "${FORMAT}" ]; then if [ -n "${TERSE}" ]; then FORMAT="%SHORTNAME% OK %CN% %DAYS_VALID%" else FORMAT="${DEFAULT_FORMAT}" fi fi # long output if [ -z "${TERSE}" ]; then EXTRA_OUTPUT="${LONG_OUTPUT}" fi # performance if [ -z "${NO_PERF}" ]; then EXTRA_OUTPUT="${EXTRA_OUTPUT}${PERFORMANCE_DATA}" fi # default protocol if [ -z "${PROTOCOL}" ]; then PROTOCOL='https' fi STATUS=OK FORMAT=$( format_template "${FORMAT}" ) if [ -z "${PROMETHEUS}" ]; then echo "${FORMAT}${EXTRA_OUTPUT}" else add_prometheus_status_output_line "cert_valid{cn=\"${CN}\"} 0" prometheus_output fi if [ -n "${INFO_OUTPUT}" ] ; then echo "${INFO_OUTPUT}" fi fi remove_temporary_files exit "${STATUS_OK}" } get_tds_certificate() { # The TDS (Tabular Data Stream) protocol used by Microsoft and # Sybase, which is an application layer protocol wrapping all # layers and connections underneath. That means a driver has to # implement this TDS protocol when connecting to Microsoft SQL # server or Sybase (TDS was created by Sybase initially). # Python script to retrieve the certificate # https://gist.github.com/lnattrass/a4a91dbf439fc1719d69f7865c1b1791#file-get_tds_cert-py debuglog "TDS: executing Python script to fetch the certificate" create_temporary_file FILE=${TEMPFILE} debuglog "Storing the certificate in ${FILE}" # to be able to use the exec_with_timeout function we store the python script in a file create_temporary_file PYTHON_SCRIPT=${TEMPFILE} cat <<____PYTHON >"${PYTHON_SCRIPT}" from __future__ import print_function import sys import pprint import struct import socket import ssl from time import sleep # Standard "HELLO" message for TDS prelogin_msg = bytearray([ 0x12, 0x01, 0x00, 0x2f, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x06, 0x01, 0x00, 0x20, 0x00, 0x01, 0x02, 0x00, 0x21, 0x00, 0x01, 0x03, 0x00, 0x22, 0x00, 0x04, 0x04, 0x00, 0x26, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]) # Prep Header function def prep_header(data): data_len = len(data) prelogin_head = bytearray([ 0x12, 0x01 ]) header_len = 8 total_len = header_len + data_len data_head = prelogin_head + total_len.to_bytes(2, 'big') data_head += bytearray([ 0x00, 0x00, 0x01, 0x00]) return data_head + data def read_header(data): if len(data) != 8: raise ValueError("prelogin header is > 8-bytes", data) format = ">bbhhbb" sct = struct.Struct(format) unpacked = sct.unpack(data) return { "type": unpacked[0], "status": unpacked[1], "length": unpacked[2], "channel": unpacked[3], "packet": unpacked[4], "window": unpacked[5] } tdspbuf = bytearray() def recv_tdspacket(sock): global tdspbuf tdspacket = tdspbuf header = {} for i in range(0,5): tdspacket += sock.recv(4096) if len(tdspacket) >= 8: header = read_header(tdspacket[:8]) if len(tdspacket) >= header['length']: tdspbuf = tdspacket[header['length']:] return header, tdspacket[8:header['length']] sleep(0.05) # Ensure we have a commandline if len(sys.argv) != 3: print("Usage: {} ".format(sys.argv[0])) sys.exit(1) hostname = sys.argv[1] port = int(sys.argv[2]) # Setup SSL if hasattr(ssl, 'PROTOCOL_TLS'): sslProto = ssl.PROTOCOL_TLS_CLIENT else: sslProto = ssl.PROTOCOL_SSLv23 sslctx = ssl.SSLContext(sslProto) sslctx.check_hostname = False sslctx.verify_mode = ssl.CERT_NONE tls_in_buf = ssl.MemoryBIO() tls_out_buf = ssl.MemoryBIO() # Create the SSLObj connected to the tls_in_buf and tls_out_buf tlssock = sslctx.wrap_bio(tls_in_buf, tls_out_buf) # create an INET, STREAMing socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setblocking(0) s.settimeout(1) # Connect to the SQL Server s.connect(( hostname, port )) # Send the first TDS PRELOGIN message s.send(prelogin_msg) # Get the response and ignore. We will try to negotiate encryption anyway. header, data = recv_tdspacket(s) while header['status']==0: header, ext_data = recv_tdspacket(s) data += ext_data # Craft the packet for i in range(0,5): try: tlssock.do_handshake() peercert = ssl.DER_cert_to_PEM_cert(tlssock.getpeercert(True)) # do not add a newline print(peercert, end='') sys.exit(0) except ssl.SSLWantReadError as err: pass tls_data = tls_out_buf.read() s.sendall(prep_header(tls_data)) # TDS Packets can be split over two frames, each with their own headers. # We have to concat these for TLS to handle nego properly header, data = recv_tdspacket(s) while header['status']==0: header, ext_data = recv_tdspacket(s) data += ext_data tls_in_buf.write(data) ____PYTHON exec_with_timeout "${PYTHON_BIN} ${PYTHON_SCRIPT} ${HOST} ${PORT} > ${FILE}" } # Defined externally # shellcheck disable=SC2154 if [ -z "${SOURCE_ONLY}" ]; then main "${@}" fi