diff --git a/certwatch.c b/certwatch.c new file mode 100644 index 0000000..2d000b1 --- /dev/null +++ b/certwatch.c @@ -0,0 +1,182 @@ +/* + Copyright 2003 Red Hat, Inc. + 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 2 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, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +/* Certificate expiry warning generation code, based on code from + * Stronghold. Joe Orton */ + +#include +#include + +#include +#include +#include + +/* Turn an ASN.1 UTCTIME object into a time_t, ish. */ +static time_t decode_utctime(const ASN1_UTCTIME *utc) +{ + struct tm tm = {0}; + int i = utc->length; + + if (i < 10) + return -1; + for (i = 0; i < 10; i++) + if ((utc->data[i] > '9') || (utc->data[i] < '0')) + return -1; + + tm.tm_year = (utc->data[0]-'0') * 10 + (utc->data[1]-'0'); + + /* Deal with Year 2000 like eay did */ + if (tm.tm_year < 70) + tm.tm_year += 100; + + tm.tm_mon = (utc->data[2]-'0') * 10 + (utc->data[3]-'0') - 1; + tm.tm_mday = (utc->data[4]-'0') * 10 + (utc->data[5]-'0'); + tm.tm_hour = (utc->data[6]-'0') * 10 + (utc->data[7]-'0'); + tm.tm_min = (utc->data[8]-'0') * 10 + (utc->data[9]-'0'); + tm.tm_sec = (utc->data[10]-'0') * 10 + (utc->data[11]-'0'); + tm.tm_isdst = -1; + + return mktime(&tm); +} + +/* Print a warning message that the certificate in 'filename', issued + * to hostname 'hostname', will expire (or has expired). */ +static int warning(FILE *out, const char *filename, const char *hostname, + time_t start, time_t end, time_t now, int quiet) +{ + int renew = 1, days = (end - now) / (3600 * 24); /* days till expiry */ + char subj[50]; + + if (start > now) { + strcpy(subj, "is not yet valid"); + renew = 0; + } else if (days < 0) { + strcpy(subj, "has expired"); + } else if (days == 0) { + strcpy(subj, "will expire today"); + } else if (days == 1) { + sprintf(subj, "will expire tomorrow"); + } else if (days < 30) { + sprintf(subj, "will expire in %d days", days); + } else { + return 0; /* nothing to warn about. */ + } + + if (quiet) return 1; + + fputs("To: root@localhost\n", out); + fprintf(out, "Subject: The certificate for %s %s\n", hostname, subj); + fputs("\n", out); + + fprintf(out, + " ################# SSL Certificate Warning ################\n\n"); + + fprintf(out, " Certificate for %s, in '%s':\n\n", hostname, filename); + + if (renew) { + fputs(" The certificate needs to be renewed; this can be done\n" + " using the 'genkey' program supplied with Red Hat\n" + " Enterprise Linux.\n\n" + " Browsers will not be able to correctly connect to this\n" + " web site using SSL until the certificate is renewed.\n", + out); + } else { + char until[30] = "(unknown date)"; + ctime_r(&start, until); + if (strlen(until) > 2) until[strlen(until)-1] = '\0'; + fprintf(out, + " The certificate is not valid until %s.\n\n" + " Browsers will not be able to correctly connect to this\n" + " web site using SSL until the certificate becomes valid.\n", + until); + } + + fputs("\n" + " ##########################################################\n" + " Generated by certwatch(8)\n\n", + out); + return 1; +} + +/* Extract the common name of 'cert' into 'buf'. */ +static int get_common_name(X509 *cert, char *buf, size_t bufsiz) +{ + X509_NAME *name = X509_get_subject_name(cert); + + if (!name) return -1; + + return X509_NAME_get_text_by_NID(name, NID_commonName, buf, bufsiz) == -1; +} + +/* Check whether the certificate in filename 'filename' has expired; + * issue a warning message if 'quiet' is zero. If quiet is non-zero, + * returns one to indicate that a warning would have been issued, zero + * to indicate no warning would be issued, or -1 if an error + * occurred. */ +static int check_cert(const char *filename, int quiet) +{ + X509 *cert; + FILE *fp; + ASN1_UTCTIME *notAfter, *notBefore; + time_t begin, end, now; + char cname[128]; + + /* parse the cert */ + if ((fp = fopen(filename, "r")) == NULL) return -1; + cert = PEM_read_X509(fp, NULL, NULL, NULL); + fclose(fp); + if (cert == NULL) return -1; + + /* determine the validity period of the cert. */ + notAfter = X509_get_notAfter(cert); + notBefore = X509_get_notBefore(cert); + + /* get time_t's out of X509 times */ + begin = decode_utctime(notBefore); + end = decode_utctime(notAfter); + now = time(NULL); + if (end == -1 || begin == -1 || now == -1) return -1; + + /* find the subject's commonName attribute */ + if (get_common_name(cert, cname, sizeof cname)) + return -1; + + X509_free(cert); + + /* don't warn about the automatically generate certificate */ + if (strcmp(cname, "localhost") == 0 || + strcmp(cname, "localhost.localdomain") == 0) + return -1; + + return warning(stdout, filename, cname, begin, end, now, quiet); +} + +int main(int argc, char **argv) +{ + int quiet = 0; + + if (argc == 3 && strcmp(argv[1], "-q") == 0) { + quiet = 1; + argc--; + argv++; + } + + if (argc != 2) return 0; + + return check_cert(argv[1], quiet) == 1 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/certwatch.cron b/certwatch.cron new file mode 100644 index 0000000..68ca6f4 --- /dev/null +++ b/certwatch.cron @@ -0,0 +1,29 @@ +#!/bin/sh +# +# Issue warning e-mails if SSL certificates expire, using +# certwatch(8). Set NOCERTWATCH=yes in /etc/sysconfig/httpd +# to disable. +# + +[ -r /etc/sysconfig/httpd ] && . /etc/sysconfig/httpd + +# Use configured httpd binary +httpd=${HTTPD-/usr/sbin/httpd} + +# Sanity checks +test -z "${NOCERTWATCH}" || exit 0 +test -x ${httpd} || exit 0 +test -x /usr/bin/certwatch || exit 0 +test -r /etc/httpd/conf/httpd.conf || exit 0 +test -x /usr/sbin/sendmail || exit 0 +test -x /etc/httpd/modules/mod_ssl.so || exit 0 + +certs=`${httpd} -t -DDUMP_CERTS 2>/dev/null` +RETVAL=$? +test $RETVAL -eq 0 || exit 0 + +for c in $certs; do + # Check whether a warning message is needed, then issue one if so. + /usr/bin/certwatch -q "$c" && + /usr/bin/certwatch "$c" | /usr/sbin/sendmail -oem -oi -t 2>/dev/null +done diff --git a/certwatch.xml b/certwatch.xml new file mode 100644 index 0000000..784e9f2 --- /dev/null +++ b/certwatch.xml @@ -0,0 +1,91 @@ + + + + + + crypto-utils + September 2004 + + + + certwatch + 1 + + + + certwatch + generate SSL certificate expiry warnings + + + + + certwatch + + filename + + + + + Description + + The certwatch program is used to issue + warning mail when an SSL certificate is about to expire. + + The program has two modes of operation: normal mode and + quiet mode. In normal mode, the certificate given by the + filename argument is examined, and a + warning email is issued to standard output if the certificate is + outside its validity period, or approaching expiry. If the + certificate cannot be found, or any errors occur whilst parsing + the certificate, the certificate is ignored and no output is + produced. + + In quiet mode (when the -q argument is + given), no output is ever produced. + + + + + Diagnostics + + In both modes of operation, the exit code indicates the + state of the certificate: + + + + 0 + + The certificate is outside its validity + period, or approaching expiry + + + + 1 + + The certificate is inside its validity + period, or could not be parsed + + + + + + Notes + + The certwatch program is run daily by + crond from the file + /etc/cron.daily/certwatch to warn about the + imminent expiry of SSL certificates configured for use in the + Apache HTTP server. This warning can be disabled by adding the + line: NOCERTWATCH=yes to the file + /etc/sysconfig/httpd. + + + + + Files + + /etc/cron.daily/certwatch + + + diff --git a/crypto-utils.spec b/crypto-utils.spec index 657a300..fe13010 100644 --- a/crypto-utils.spec +++ b/crypto-utils.spec @@ -3,14 +3,17 @@ Summary: SSL certificate and key management utilities Name: crypto-utils -Version: 2.0 -Release: 6 +Version: 2.1 +Release: 1 Source: crypto-rand-%{crver}.tar.gz Source1: genkey.pl +Source2: certwatch.c +Source3: certwatch.cron +Source4: certwatch.xml Group: Applications/System License: Various BuildRoot: %{_tmppath}/%{name}-%{version}-root -BuildPreReq: openssl-devel, perl +BuildRequires: openssl-devel, perl, pkgconfig Requires: newt-perl, openssl Requires: %(eval `perl -V:version`; echo "perl(:MODULE_COMPAT_$version)") Obsoletes: crypto-rand @@ -23,9 +26,13 @@ SSL certificates and keys. %setup -q -n crypto-rand-%{crver} %build -%configure --with-newt=%{_prefix} CFLAGS="-fPIC" +%configure --with-newt=%{_prefix} CFLAGS="-fPIC $RPM_OPT_FLAGS" make +cc $RPM_OPT_FLAGS -Wall -Werror -I/usr/include/openssl -o certwatch \ + $RPM_SOURCE_DIR/certwatch.c -lcrypto +xmlto man $RPM_SOURCE_DIR/certwatch.xml + pushd Makerand perl -pi -e "s/Stronghold/Crypt/g" * CFLAGS="$RPM_OPT_FLAGS" perl Makefile.PL PREFIX=$RPM_BUILD_ROOT/usr INSTALLDIRS=vendor @@ -33,6 +40,7 @@ make popd %install +rm -rf $RPM_BUILD_ROOT pushd Makerand make install @@ -51,12 +59,22 @@ find $RPM_BUILD_ROOT/usr -type f -print | grep -v "\.packlist" > filelist if [ ! -s filelist ] ; then echo "ERROR: EMPTY FILE LIST" - exit -1 + exit 1 fi +mkdir -p $RPM_BUILD_ROOT%{_sysconfdir}/cron.daily \ + $RPM_BUILD_ROOT%{_mandir}/man1 \ + $RPM_BUILD_ROOT%{_bindir} + # install keyrand -%{__mkdir_p} $RPM_BUILD_ROOT%{_bindir} -install -m 755 keyrand/keyrand $RPM_BUILD_ROOT%{_bindir}/keyrand +install -c -m 755 keyrand/keyrand $RPM_BUILD_ROOT%{_bindir}/keyrand + +# install certwatch +install -c -m 755 certwatch $RPM_BUILD_ROOT%{_bindir}/certwatch +install -c -m 755 $RPM_SOURCE_DIR/certwatch.cron \ + $RPM_BUILD_ROOT%{_sysconfdir}/cron.daily/certwatch +install -c -m 644 certwatch.1 \ + $RPM_BUILD_ROOT%{_mandir}/man1/certwatch.1 # install genkey sed -e "s|^\$bindir.*$|\$bindir = \"/usr/bin\";|" \ @@ -73,8 +91,14 @@ sed -e "s|^\$bindir.*$|\$bindir = \"/usr/bin\";|" \ %files -f filelist %defattr(0644,root,root,0755) %attr(0755,root,root) %{_bindir}/* +%{_sysconfdir}/cron.daily/certwatch +%{_mandir}/man1/certwatch.1* %changelog +* Fri Sep 10 2004 Joe Orton 2.1-1 +- add /usr/bin/certwatch +- support --days argument to genkey (#131045) + * Tue Aug 17 2004 Joe Orton 2.0-6 - add perl MODULE_COMPAT requirement diff --git a/genkey.pl b/genkey.pl index a4c02a1..c6e6a91 100644 --- a/genkey.pl +++ b/genkey.pl @@ -25,6 +25,7 @@ # 200305 Hide passwords entered for private key # 200308 Adapted for Taroon # 200308 Fix warnings in UTF-8 locale +# 200409 Added --days support # # $bindir = "%INSTDIR%/bin"; @@ -64,6 +65,7 @@ Usage: genkey [options] servername --test Test mode, skip random data creation, overwrite existing key --genreq Just generate a CSR from an existing key --makeca Generate a private CA key instead + --days Days until expiry of self-signed certificate (default 30) EOH exit 1; } @@ -109,8 +111,10 @@ sub RunForm my $test_mode = ''; my $genreq_mode = ''; my $ca_mode = ''; +my $cert_days = 30; GetOptions('test|t' => \$test_mode, 'genreq' => \$genreq_mode, + 'days=i' => \$cert_days, 'makeca' => \$ca_mode) or usage(); usage() unless @ARGV != 0; $skip_random = $test_mode; @@ -877,7 +881,7 @@ sub genReqWindow if (!$genreq_mode) { if (!-f $certfile) { - makeCert($keyfile,$certfile,$cadetails,"-days 30 -x509"); + makeCert($keyfile,$certfile,$cadetails,"-days $cert_days -x509"); } } @@ -981,7 +985,7 @@ sub genCertWindow my $ret = getCertDetails($servername,$msg, 0); return $ret unless ($ret eq "Next"); - makeCert($keyfile,$certfile,$cadetails,"-days 30 -x509"); + makeCert($keyfile,$certfile,$cadetails,"-days $cert_days -x509"); return "Next"; }