check_ssl_validity.pl 9.3 KB

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