check_ssl_validity.pl 11 KB

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