check_ssl_validity.pl 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. #! /usr/bin/perl
  2. # nagios: -epn
  3. # Complete (?) check for valid SSL certificate
  4. # Originally by Anders Nordby (anders@fupp.net), 2015-02-16
  5. # Copied with permission on 2019-09-26 (https://github.com/nagios-plugins/nagios-plugins/issues/72)
  6. # and modified to fit the needs of the nagios-plugins project.
  7. # Copyright: GPLv2
  8. # Checks all of the following:
  9. # Fetch SSL certificate from URL (on optional given host)
  10. # Does the certificate contain our hostname?
  11. # Has the certificate expired?
  12. # Download (and cache) CRL
  13. # Has the certificate been revoked?
  14. use Getopt::Std;
  15. use File::Temp qw(tempfile);
  16. use Crypt::X509;
  17. use Date::Parse;
  18. use POSIX qw(strftime);
  19. use Digest::MD5 qw(md5_hex);
  20. use LWP::Simple;
  21. getopts('p:t:H:dw:c:I:C:d');
  22. sub usage {
  23. 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";
  24. print "\nWill look for hostname provided with -H in the certificate, but will contact\n";
  25. print "server with host/IP provided by -I (optional)\n";
  26. exit(1);
  27. }
  28. sub updatecrl {
  29. my $url = shift;
  30. my $fn = shift;
  31. my $content = get($url);
  32. if (defined($content)) {
  33. if (open(CACHE, ">$cachefile")) {
  34. print CACHE $content;
  35. } else {
  36. doexit(2, "Could not open file $fn for writing CRL temp file for cert on $host:$port.");
  37. }
  38. close(CACHE);
  39. } else {
  40. doexit(2, "Could not download CRL Distribution Point URL $url for cert on $hosttxt.");
  41. }
  42. }
  43. sub ckserial {
  44. return if ($crserial eq "");
  45. if ($serial eq $crserial) {
  46. if ($crrev ne "") {
  47. $crrevtime = str2time($crrev);
  48. $revtime = $crrevtime-$uxtime;
  49. if ($revtime < 0) {
  50. doexit(2, "Found certificate for $vhost on CRL $crldp revoked already at date $crrev");
  51. } elsif (($revtime/86400) < $crit) {
  52. doexit(2, "Found certificate for $vhost on CRL $crldp revoked at date $crrev, within critical time frame $crit");
  53. } elsif (($revtime/86400) < $warn) {
  54. doexit(1, "Found certificate for $vhost on CRL $crldp revoked at date $crrev, within warning time frame $warn");
  55. }
  56. }
  57. doexit(1, "Found certificate for $vhost on CRL $crldp revoked $crrev. Time to check the revokation date");
  58. }
  59. }
  60. usage unless ($opt_H);
  61. # Defaults
  62. if ($opt_p) {
  63. $port = $opt_p;
  64. } else {
  65. $port = 443;
  66. }
  67. if ($opt_t) {
  68. $tmout = $opt_t;
  69. } else {
  70. $tmout = 10;
  71. }
  72. if ($opt_C) {
  73. $crlupdatefreq = $opt_C;
  74. } else {
  75. $crlupdatefreq = 86400;
  76. }
  77. $vhost = $opt_H;
  78. if ($opt_I) {
  79. $host = $opt_I;
  80. } else {
  81. $host = $vhost;
  82. }
  83. $hosttxt = "$host:$port";
  84. if ($opt_w && $opt_w =~ /^\d+$/) {
  85. $warn = $opt_w;
  86. } else {
  87. $warn = 30;
  88. }
  89. if ($opt_c && $opt_c =~ /^\d+$/) {
  90. $crit = $opt_c;
  91. } else {
  92. $crit = 30;
  93. }
  94. sub doexit {
  95. my $ret = shift;
  96. my $txt = shift;
  97. print "$txt\n";
  98. exit($ret);
  99. }
  100. $alldata = "";
  101. $cert = "";
  102. $mode = 0;
  103. open(CMD, "echo | openssl s_client -servername $vhost -connect $host:$port 2>&1 |");
  104. while (<CMD>) {
  105. $alldata .= $_;
  106. if ($mode == 0) {
  107. if (/-----BEGIN CERTIFICATE-----/) {
  108. $cert .= $_;
  109. $mode = 1;
  110. }
  111. } elsif ($mode == 1) {
  112. $cert .= $_;
  113. if (/-----END CERTIFICATE-----/) {
  114. $mode = 2;
  115. }
  116. }
  117. }
  118. close(CMD);
  119. $ret = $?;
  120. if ($ret != 0) {
  121. $alldata =~ s@\n@ @g;
  122. $alldata =~ s@\s+$@@;
  123. doexit(2, "Error connecting to $hosttxt: $alldata");
  124. } elsif ($cert eq "") {
  125. doexit(2, "No certificate found on $hosttxt");
  126. } else {
  127. ($tmpfh,$tempfile) = tempfile(DIR=>'/tmp',UNLINK=>0);
  128. doexit(2, "Failed to open temp file: $!") unless (defined($tmpfh));
  129. $tmpfh->print($cert);
  130. $tmpfh->close;
  131. }
  132. $dercert = `openssl x509 -in $tempfile -outform DER 2>&1`;
  133. $ret = $?;
  134. if ($ret != 0) {
  135. $dercert =~ s@\n@ @g;
  136. $dercert =~ s@\s+$@@;
  137. doexit(2, "Could not convert certificate from PEM to DER format: $dercert");
  138. }
  139. $decoded = Crypt::X509->new( cert => $dercert );
  140. if ($decoded->error) {
  141. doexit(2, "Could not parse X509 certificate on $hosttxt: " . $decoded->error);
  142. }
  143. $oktxt = "";
  144. $cn = $decoded->subject_cn;
  145. if ($opt_d) { print "Found CN: $cn\n"; }
  146. if ($vhost eq $decoded->subject_cn) {
  147. $oktxt .= "Host $vhost matches CN $vhost on $hosttxt ";
  148. } elsif ($decoded->subject_cn =~ /^*\.(.*)$/) {
  149. $wcdomain = $1;
  150. $domain = $vhost;
  151. $domain =~ s@^[\w\-]+\.@@;
  152. if ($domain eq $wcdomain) {
  153. $oktxt .= "Host $vhost matches wildcard CN " . $decoded->subject_cn . " on $hosttxt ";
  154. }
  155. }
  156. if ($oktxt eq "") {
  157. # Cert not yet found
  158. if (defined($decoded->SubjectAltName)) {
  159. # Check altnames
  160. $altfound = 0;
  161. foreach $altnametxt (@{$decoded->SubjectAltName}) {
  162. if ($altnametxt =~ /^dNSName=(.*)/) {
  163. $altname = $1;
  164. if ($opt_d) { print "Found SAN: $altname\n"; }
  165. if ($vhost eq $altname) {
  166. $altfound = 1;
  167. $oktxt .= "Host $vhost found in SAN on $hosttxt ";
  168. last;
  169. }
  170. }
  171. }
  172. if ($altfound == 0) {
  173. doexit(2, "Host $vhost not found in certificate on $hosttxt, not in CN or in alternative names");
  174. }
  175. } else {
  176. doexit(2, "Host $vhost not found in certificate on $hosttxt, not in CN and no alternative names found");
  177. }
  178. }
  179. # Check expire time
  180. $uxtimegmt = strftime "%s", gmtime;
  181. $uxtime = strftime "%s", localtime;
  182. $certtime = $decoded->not_after;
  183. $certdays = ($certtime-$uxtimegmt)/86400;
  184. $certdaysfmt = sprintf("%.1f", $certdays);
  185. if ($certdays < $crit) {
  186. doexit(2, "${oktxt}but it is expiring in only $certdaysfmt days, critical limit is $crit.");
  187. } elsif ($certdays < $warn) {
  188. doexit(1, "${oktxt}but it is expiring in only $certdaysfmt days, warning limit is $warn.");
  189. }
  190. $serial = $decoded->serial;
  191. $serial = lc(sprintf("%x", $serial));
  192. if ($opt_d) {
  193. print "Certificate serial: $serial\n";
  194. }
  195. @crldps = @{$decoded->CRLDistributionPoints};
  196. $crlskip = 0;
  197. foreach $crldp (@crldps) {
  198. if ($opt_d) {
  199. print "Checking CRL DP $crldp.\n";
  200. }
  201. $cachefile = "/tmp/" . md5_hex($crldp) . "_crl.tmp";
  202. if (-f $cachefile) {
  203. $cacheage = $uxtime-(stat($cachefile))[9];
  204. if ($cacheage > $crlupdatefreq) {
  205. if ($opt_d) { print "Download update, more than a day old.\n"; }
  206. updatecrl($crldp, $cachefile);
  207. } else {
  208. if ($opt_d) { print "Reusing cached copy.\n"; }
  209. }
  210. } else {
  211. if ($opt_d) { print "Download initial copy.\n"; }
  212. updatecrl($crldp, $cachefile);
  213. }
  214. $crl = "";
  215. my $format;
  216. open(my $cachefile_io, '<', $cachefile);
  217. $format = <$cachefile_io> =~ /-----BEGIN X509 CRL-----/ ? 'PEM' : 'DER';
  218. close $cachefile_io;
  219. open(CMD, "openssl crl -inform $format -text -in $cachefile -noout 2>&1 |");
  220. while (<CMD>) {
  221. $crl .= $_;
  222. }
  223. close(CMD);
  224. $ret = $?;
  225. if ($ret != 0) {
  226. $crl =~ s@\n@ @g;
  227. $crl =~ s@\s+$@@;
  228. doexit(2, "Could not parse $format from URL $crldp while checking $hosttxt: $crl");
  229. }
  230. # Crude CRL parsing goes here
  231. $mode = 0;
  232. foreach $cline (split(/\n/, $crl)) {
  233. if ($cline =~ /.*Next Update: (.+)/) {
  234. $nextup = $1;
  235. $nextuptime = str2time($nextup);
  236. $crlvalid = $nextuptime-$uxtime;
  237. if ($opt_d) { print "Next CRL update: $nextup\n"; }
  238. if ($crlvalid < 0) {
  239. doexit(2, "Could not use CRL from $crldp, it expired past next update on $nextup");
  240. }
  241. } elsif ($cline =~ /.*Last Update: (.+)/) {
  242. $lastup = $1;
  243. if ($opt_d) { print "Last CRL update: $lastup\n"; }
  244. } elsif ($mode == 0) {
  245. if ($cline =~ /.*Serial Number: (\S+)/i) {
  246. ckserial;
  247. $crserial = lc($1);
  248. $crrev = "";
  249. } elsif ($cline =~ /.*Revocation Date: (.+)/i) {
  250. $crrev = $1;
  251. }
  252. } elsif ($cline =~ /Signature Algorithm/) {
  253. last;
  254. }
  255. }
  256. ckserial;
  257. }
  258. if (-f $tempfile) {
  259. unlink ($tempfile);
  260. }
  261. $oktxt =~ s@\s+$@@;
  262. print "$oktxt, still valid for $certdaysfmt days. ";
  263. if ($crlskip == 0) {
  264. print "Serial $serial not found on any Certificate Revokation Lists.\n";
  265. } else {
  266. print "CRL checks skipped, next check in " . ($crlupdatefreq - $cacheage) . " seconds.\n";
  267. }
  268. exit 0;