File check_ssl_validity of Package monitoring-plugins-ssl_validity
#! /usr/bin/perl
# nagios: -epn
# Complete (?) check for valid SSL certificate
# anders@fupp.net, 2015-02-16
# Checks all of the following:
# Fetch SSL certificate from URL (on optional given host)
# Does the certificate contain our hostname?
# Has the certificate expired?
# Download (and cache) CRL
# Has the certificate been revoked?
use Getopt::Std;
use File::Temp qw(tempfile);
use Crypt::X509;
use Date::Parse;
use POSIX qw(strftime);
use Digest::MD5 qw(md5_hex);
use LWP::Simple;
getopts('p:t:H:dw:c:I:C:d');
sub usage {
print "check_ssl_validity -H <cert hostname> [-I <IP/host>] [-p <port>]\n[-t <timeout>] [-w <expire warning (days)>] [-c <expire critical (dats)>]\n[-C (CRL update frequency in seconds)] [-d (debug)]\n";
print "\nWill look for hostname provided with -H in the certificate, but will contact\n";
print "server with host/IP provided by -I (optional)\n";
exit(1);
}
sub updatecrl {
my $url = shift;
my $fn = shift;
my $content = get($url);
if (defined($content)) {
if (open(CACHE, ">$cachefile")) {
print CACHE $content;
} else {
doexit(2, "Could not open file $fn for writing CRL temp file for cert on $host:$port.");
}
close(CACHE);
} else {
doexit(2, "Could not download CRL Distribution Point URL $url for cert on $hosttxt.");
}
}
sub ckserial {
return if ($crserial eq "");
# if ($serial eq "3ddf324101d0de0d") {
if ($serial eq $crserial) {
if ($crrev ne "") {
$crrevtime = str2time($crrev);
$revtime = $crrevtime-$uxtime;
# print "revtime: $revtime\n";
if ($revtime < 0) {
doexit(2, "Found certificate for $vhost on CRL $crldp revoked already at date $crrev");
} elsif (($revtime/86400) < $crit) {
doexit(2, "Found certificate for $vhost on CRL $crldp revoked at date $crrev, within critical time frame $crit");
} elsif (($revtime/86400) < $warn) {
doexit(1, "Found certificate for $vhost on CRL $crldp revoked at date $crrev, within warning time frame $warn");
}
}
doexit(1, "Found certificate for $vhost on CRL $crldp revoked $crrev. Time to check the revokation date");
}
}
usage unless ($opt_H);
# Defaults
if ($opt_p) {
$port = $opt_p;
} else {
$port = 443;
}
if ($opt_t) {
$tmout = $opt_t;
} else {
$tmout = 10;
}
if ($opt_C) {
$crlupdatefreq = $opt_C;
} else {
$crlupdatefreq = 86400;
}
$vhost = $opt_H;
if ($opt_I) {
$host = $opt_I;
} else {
$host = $vhost;
}
$hosttxt = "$host:$port";
if ($opt_w && $opt_w =~ /^\d+$/) {
$warn = $opt_w;
} else {
$warn = 30;
}
if ($opt_c && $opt_c =~ /^\d+$/) {
$crit = $opt_c;
} else {
$crit = 30;
}
sub doexit {
my $ret = shift;
my $txt = shift;
print "$txt\n";
exit($ret);
}
$alldata = "";
$cert = "";
$mode = 0;
open(CMD, "echo | openssl s_client -servername $vhost -connect $host:$port 2>&1 |");
while (<CMD>) {
$alldata .= $_;
if ($mode == 0) {
if (/-----BEGIN CERTIFICATE-----/) {
$cert .= $_;
$mode = 1;
}
} elsif ($mode == 1) {
$cert .= $_;
if (/-----END CERTIFICATE-----/) {
$mode = 2;
}
}
}
close(CMD);
$ret = $?;
if ($ret != 0) {
$alldata =~ s@\n@ @g;
$alldata =~ s@\s+$@@;
doexit(2, "Error connecting to $hosttxt: $alldata");
} elsif ($cert eq "") {
doexit(2, "No certificate found on $hosttxt");
} else {
($tmpfh,$tempfile) = tempfile(DIR=>'/tmp',UNLINK=>0);
doexit(2, "Failed to open temp file: $!") unless (defined($tmpfh));
$tmpfh->print($cert);
$tmpfh->close;
}
$dercert = `openssl x509 -in $tempfile -outform DER 2>&1`;
$ret = $?;
if ($ret != 0) {
$dercert =~ s@\n@ @g;
$dercert =~ s@\s+$@@;
doexit(2, "Could not convert certificate from PEM to DER format: $dercert");
}
$decoded = Crypt::X509->new( cert => $dercert );
if ($decoded->error) {
doexit(2, "Could not parse X509 certificate on $hosttxt: " . $decoded->error);
}
$oktxt = "";
$cn = $decoded->subject_cn;
if ($opt_d) { print "Found CN: $cn\n"; }
if ($vhost eq $decoded->subject_cn) {
$oktxt .= "Host $vhost matches CN $vhost on $hosttxt ";
} elsif ($decoded->subject_cn =~ /^*\.(.*)$/) {
$wcdomain = $1;
$domain = $vhost;
$domain =~ s@^[\w\-]+\.@@;
if ($domain eq $wcdomain) {
$oktxt .= "Host $vhost matches wildcard CN " . $decoded->subject_cn . " on $hosttxt ";
}
}
if ($oktxt eq "") {
# Cert not yet found
if (defined($decoded->SubjectAltName)) {
# Check altnames
$altfound = 0;
foreach $altnametxt (@{$decoded->SubjectAltName}) {
if ($altnametxt =~ /^dNSName=(.*)/) {
$altname = $1;
if ($opt_d) { print "Found SAN: $altname\n"; }
if ($vhost eq $altname) {
$altfound = 1;
$oktxt .= "Host $vhost found in SAN on $hosttxt ";
last;
}
}
}
if ($altfound == 0) {
doexit(2, "Host $vhost not found in certificate on $hosttxt, not in CN or in alternative names");
}
} else {
doexit(2, "Host $vhost not found in certificate on $hosttxt, not in CN and no alternative names found");
}
}
# Check expire time
$uxtimegmt = strftime "%s", gmtime;
$uxtime = strftime "%s", localtime;
$certtime = $decoded->not_after;
$certdays = ($certtime-$uxtimegmt)/86400;
$certdaysfmt = sprintf("%.1f", $certdays);
if ($certdays < $crit) {
doexit(2, "${oktxt}but it is expiring in only $certdaysfmt days, critical limit is $crit.");
} elsif ($certdays < $warn) {
doexit(1, "${oktxt}but it is expiring in only $certdaysfmt days, warning limit is $warn.");
}
$serial = $decoded->serial;
$serial = lc(sprintf("%x", $serial));
if ($opt_d) {
print "Certificate serial: $serial\n";
}
@crldps = @{$decoded->CRLDistributionPoints};
$crlskip = 0;
foreach $crldp (@crldps) {
if ($opt_d) {
print "Checking CRL DP $crldp.\n";
}
$cachefile = "/tmp/" . md5_hex($crldp) . "_crl.tmp";
if (-f $cachefile) {
$cacheage = $uxtime-(stat($cachefile))[9];
if ($cacheage > $crlupdatefreq) {
if ($opt_d) { print "Download update, more than a day old.\n"; }
updatecrl($crldp, $cachefile);
} else {
if ($opt_d) { print "Reusing cached copy of it.\n"; }
# print "Reuse CRL DP cachefile for $crldp, less than a day old.\n";
# No need to check CRL, it has already been so? Well we could have many certs to check.
# $crlskip = 1;
# next;
}
} else {
if ($opt_d) { print "Download initial copy.\n"; }
updatecrl($crldp, $cachefile);
}
# print "Check CRL DP $crldp $cachefile\n";
$crl = "";
my $format;
open(my $cachefile_io, '<', $cachefile);
$format = <$cachefile_io> =~ /-----BEGIN X509 CRL-----/ ? 'PEM' : 'DER';
close $cachefile_io;
open(CMD, "openssl crl -inform $format -text -in $cachefile -noout 2>&1 |");
while (<CMD>) {
$crl .= $_;
}
close(CMD);
$ret = $?;
if ($ret != 0) {
$crl =~ s@\n@ @g;
$crl =~ s@\s+$@@;
doexit(2, "Could not parse $format from URL $crldp while checking $hosttxt: $crl");
}
# Crude CRL parsing goes here
$mode = 0;
foreach $cline (split(/\n/, $crl)) {
# print "cline=$cline\n";
if ($cline =~ /.*Next Update: (.+)/) {
$nextup = $1;
$nextuptime = str2time($nextup);
$crlvalid = $nextuptime-$uxtime;
if ($opt_d) { print "Next CRL update: $nextup\n"; }
# print "crlvalid: $crlvalid\n";
if ($crlvalid < 0) {
doexit(2, "Could not use CRL from $crldp, it expired past next update on $nextup");
}
# print "nextuptime $nextuptime nextup $nextup X\n";
} elsif ($cline =~ /.*Last Update: (.+)/) {
$lastup = $1;
if ($opt_d) { print "Last CRL update: $lastup\n"; }
# $lastuptime = str2time($lastup);
# print "lastuptime $lastuptime lastup $lastup X\n";
} elsif ($mode == 0) {
if ($cline =~ /.*Serial Number: (\S+)/i) {
ckserial;
$crserial = lc($1);
$crrev = "";
} elsif ($cline =~ /.*Revocation Date: (.+)/i) {
$crrev = $1;
}
} elsif ($cline =~ /Signature Algorithm/) {
last;
}
}
ckserial;
}
if (-f $tempfile) {
unlink ($tempfile);
}
$oktxt =~ s@\s+$@@;
print "$oktxt, still valid for $certdaysfmt days. ";
if ($crlskip == 0) {
print "Serial $serial not found on any Certificate Revokation Lists.\n";
} else {
print "CRL checks skipped, next check in " . ($crlupdatefreq - $cacheage) . " seconds.\n";
}
exit 0;