Certificate Authority Manager



  • Certificate Authority Manager

    may add in the future? after checking the number of days prior to the expiration of the certificate. and screw for example a script found on the Internet.
    –---
    It turns out that to test enough to go through all ~ / .TinyCA / CAName / certs / *. Pem files. Script to perl, which checks the validity of the certificate. Script exits with code 2 if the certificate has expired, with code 1 if expire in two weeks and zero if the certificate is OK.

    run: check_certs /usr/local/OpenSSL/newcerts

    check_certs

    
    #!/bin/sh
    
    if [ ! -d "$1" ]; then
        echo "Unable to find directory: $1"
        exit 1
    fi
    
    for sslcert in "$1"/*.pem
    do
        if [ -f "$sslcert" ]; then
            /usr/local/OpenSSL/check/check-certificate-expire "$sslcert"
        else
            echo "File $sslcert not found"
        fi
    done
    
    

    check-expire

    
    #!/usr/bin/perl
    #
    # Expiration date checker and handler. Supports x509 certificates, and
    # anything a wrapper script can be written for. Run perldoc(1) on this
    # script for additional documentation.
    #
    # The author disclaims all copyrights and releases this script into the
    # public domain.
    
    use strict;
    use warnings;
    
    use Config::General qw(ParseConfig);
    use Date::Parse qw(str2time);
    use File::Basename qw(basename);
    use File::Temp qw(tempfile);
    use Getopt::Std;
    
    END {
      # Report problems when writing to stdout (perldoc perlopentut)
      unless ( close(STDOUT) ) {
        warn "error: problem closing STDOUT: $!\n";
        exit 74;
      }
    }
    
    my $basename = basename($0);
    
    my %program_param = ( name => $basename, argv => "@ARGV", pid => $$ );
    
    # list different modes of operation (subroutine that obtains expiration
    # date and other metadata from an external command/code)
    my %actions = (
      certificate  => \&action_certificate,
      openssl_x509 => \&action_openssl_x509,
    );
    my @actions = sort keys %actions;
    
    my %opts;
    getopts 'h?l:c:f:', \%opts;
    
    if ( exists $opts{l} ) {
      print "@actions\n";
      do_exit(0);
    }
    
    if ( exists $opts{h} or exists $opts{'?'} or not @ARGV ) {
      print_help();
      do_exit(64);
    }
    
    my %config = ParseConfig( -ConfigFile => $opts{f}, -LowerCaseNames => 1 );
    if ( !defined $config{class} ) {
      remark(
        'error',
        'no classes defined in configuration',
        { file => $opts{f} }
      );
      do_exit(78);
    }
    
    my $op_mode = shift || q{};
    $op_mode =~ tr/-/_/;
    
    if ( !exists $actions{$op_mode} ) {
      remark( 'error', 'unknown mode',
        { allowed => join( q{,}, @actions ), mode => $op_mode } );
      print_help();
      do_exit(78);
    }
    
    # where in the prefs to read expire, window handlers from
    my $report_handler = exists $opts{c} ? $opts{c} : 'default';
    my $report_ref = $config{class}->{$report_handler};
    
    if ( !defined $report_ref ) {
      remark(
        'error',
        'class preferences not found',
        { class => $report_handler, file => $opts{f} }
      );
      do_exit(78);
    }
    
    # lookup expiration date and other info via mode handler
    my ( %time_param, %extra_param );
    {
      my ( $time_ref, $extra_ref ) = $actions{$op_mode}->(@ARGV);
      if ( !defined $time_ref or !defined $time_ref->{expire_epoch} ) {
        remark( 'error', 'no expire date found' );
        do_exit(78);
      }
      @time_param{ keys %$time_ref }   = values %$time_ref;
      @extra_param{ keys %$extra_ref } = values %$extra_ref;
    }
    $time_param{cur_epoch} = time;
    
    my %lookup = (
      time    => \%time_param,
      extra   => \%extra_param,
      program => \%program_param
    );
    
    # check whether expired, or if within a window
    if ( $time_param{expire_epoch} <= $time_param{cur_epoch} ) {
    
      # TODO fill out more parameters (humanized time? strftime, parameters
      # about the certificate, and etc.? - see what need for reporting)
      # seconds_ago  => $cur_epoch - $expire_epoch,
      handle_condition( $report_ref->{expired}, \%lookup );
    
      die "error: expired handler did not exit script\n";
    }
    
    if ( exists $report_ref->{window} ) {
      my @windows;
    
      if ( ref $report_ref->{window} eq 'HASH' ) {
        my %tmp = %{ $report_ref->{window} };
        delete $report_ref->{window};
        $report_ref->{window}->[0] = \%tmp;
      } elsif ( ref $report_ref->{window} ne 'ARRAY' ) {
        remark(
          'error',
          'unexpected reference type for window',
          { ref => ref $report_ref->{window}, file => $opts{f} }
        );
        do_exit(78);
      }
    
      # build Windows to look at (sanity checks, duration conversion)
      # TODO include window timevalue in time_param hash
      for my $window_ref ( @{ $report_ref->{window} } ) {
        if ( !exists $window_ref->{inside} ) {
          remark( 'error', 'skipping window without date setting' );
          next;
        }
        $window_ref->{inside_sec} = duration2seconds( $window_ref->{inside} );
        push @windows, $window_ref;
      }
    
      for
        my $window_ref ( sort { $a->{inside_sec} <=> $b->{inside_sec} } @windows )
      {
    
        if ( $time_param{expire_epoch} <=
          ( $time_param{cur_epoch} + $window_ref->{inside_sec} ) ) {
    
          # TODO seconds_left => $expire_epoch - $cur_epoch,
          handle_condition( $window_ref, \%lookup );
    
          die "error: window handler did not exit script\n";
        }
      }
    }
    
    if ( exists $report_ref->{default} ) {
      # TODO seconds_left => $expire_epoch - $cur_epoch,
      handle_condition( $report_ref->{default}, \%lookup );
    
      die "error: default handler did not exit script\n";
    } else {
      # if no default, exit, assuming an implicit default exit
      do_exit(0);
    }
    
    # Mode to parse for expiration date using 'openssl x509' (certificate)
    sub action_openssl_x509 {
      my @x509_arguments = @_;
    
      my @command =
        ( qw{openssl x509 -noout -dates -subject -issuer}, @x509_arguments );
    
      my $results = get_output(@command);
      if ( !defined $results ) {
        remark( 'error', 'no output returned', { command => "@command" } );
        do_exit(70);
      }
    
      my ( %time_param, %cert_param );
      for my $line (@$results) {
        if ( $line =~ m/^notAfter=(.+)/ ) {
          $time_param{expire_epoch} = date_openssl2epoch($1)
            if !exists $time_param{expire_epoch};
          next;
        }
        if ( $line =~ m/^notBefore=(.+)/ ) {
          $time_param{start_epoch} = date_openssl2epoch($1)
            if !exists $time_param{start_epoch};
          next;
        }
        if ( $line =~ m/^subject\s*=\s*(.+)/ ) {
          $cert_param{subject} = $1 if !exists $cert_param{subject};
          next;
        }
        if ( $line =~ m/^issuer\s*=\s*(.+)/ ) {
          $cert_param{issuer} = $1 if !exists $cert_param{issuer};
          next;
        }
      }
    
      if ( !exists $time_param{expire_epoch} ) {
        return;
      }
    
      return \%time_param, \%cert_param;
    }
    
    # runs a command that should return a certificate to stdout that
    # 'openssl x509' can then parse for information
    sub action_certificate {
      my @command = @_;
      my $results = get_output(@command);
      if ( !defined $results ) {
        remark( 'error', 'no output returned', { command => "@command" } );
        do_exit(70);
      }
    
      my ( $tmp_fh, $filename );
      eval { ( $tmp_fh, $filename ) = tempfile(); };
    
      local $SIG{TERM} = sub { close $tmp_fh; unlink $filename };
    
      if ( $@ or !defined $tmp_fh ) {
        remark(
          'error',
          'no temporary file created',
          ( $@ ? do { chomp $@; { errstr => $@ } } : {} )
        );
        do_exit(73);
      }
    
      for my $line (@$results) {
        print $tmp_fh $line;
      }
      close $tmp_fh;
    
      return $actions{openssl_x509}->( '-in', $filename );
    }
    
    # parses handler prefs from under config, figures out what to do...
    sub handle_condition {
      my $handle_ref = shift;
      my $lookup_ref = shift;
    
      # handlers must exit, default to 0 if unset
      if ( !exists $handle_ref->{exit_value} ) {
        $handle_ref->{exit_value} = 0;
      }
    
      my %handlers = (
        # runs a command, such as logger(1)
        exec => sub {
          my $cmd_str    = shift;
          my $lookup_ref = shift;
    
          my @command = parse_tokens($cmd_str);
          for my $part (@command) {
            $part =~
              s/ (?{$1}->{$2} || '' /egx;
            $part =~ s/(\\.)/qq("$1")/eeg;
          }
    
          my $status = system @command;
          if ( $status != 0 ) {
            remark(
              'warning',
              'command failed',
              { command => "@command", errno => $? }
            );
          }
    
          return;
        },
        # like exec, but takes first token as standard input to the program
        # (to support sending data to things like mail(1))
        pipe => sub {
          my $cmd_str    = shift;
          my $lookup_ref = shift;
    
          # standard input everything before first unbackwhacked |
          my ( $stdin, $cmd_tmp ) = split /\s*(?
          $stdin =~
            s/ (?{$1}->{$2} || '' /egx;
          $stdin =~ s/(\\.)/qq("$1")/eeg;
    
          my @command;
          for my $part ( parse_tokens($cmd_tmp) ) {
            $part =~
              s/ (?{$1}->{$2} || '' /egx;
            $part =~ s/(\\.)/qq("$1")/eeg;
            push @command, $part;
          }
    
          my $cmd_fh;
          open $cmd_fh, '|-' or exec @command or return;
          print $cmd_fh $stdin;
          close $cmd_fh;
    
          return;
        },
        # print to standard output
        stdout => sub {
          my $output_str = shift;
          my $lookup_ref = shift;
    
          $output_str =~
            s/ (?{$1}->{$2} || '' /egx;
          $output_str =~ s/(\\.)/qq("$1")/eeg;
          print "$output_str\n";
    
          return;
        }
      );
    
      for my $handle ( sort keys %handlers ) {
        if ( exists $handle_ref->{$handle} ) {
          if ( ref $handle_ref->{$handle} eq 'ARRAY' ) {
            for my $item ( @{ $handle_ref->{$handle} } ) {
              $handlers{$handle}->( $item, $lookup_ref );
            }
          } else {
            $handlers{$handle}->( $handle_ref->{$handle}, $lookup_ref );
          }
        }
      }
    
      do_exit( $handle_ref->{exit_value} );
    }
    
    # converts a string into a list of tokens
    sub parse_tokens {
      my $string = shift;
      my @tokens;
    
    UBLE: {
        # non-quoted strings, backslashed quotes and whitespace allowed
        push( @tokens, $1 ), redo UBLE if $string =~ m/ \G ( [^"'\s]+ ) \s* /cgx;
    
        # double-quoted strings, backslashed quotes allowed
        push( @tokens, $1 ), redo UBLE
          if $string =~ m/ \G " ((?: \\.|[^\\"] )+) " \s* /cgx;
    
        push( @tokens, $1 ), redo UBLE
          if $string =~ m/ \G ' ((?: \\.|[^\\'] )+) ' \s* /cgx;
    
        last UBLE if $string =~ / \G $ /gcx;
    
        remark( 'error', 'unparseable token in string', { data => $string } );
        do_exit(78);
      }
    
      return @tokens;
    }
    
    sub date_openssl2epoch {
      my $date = shift;
    
      my $time = str2time($date);
      return $time;
    }
    
    # takes command to run (and optional leading hashref with parameters),
    # returns filehandle (or undef on error) with STDOUT of program
    sub get_output {
      my $param = {};
      if ( @_ and ref $_[0] eq 'HASH' ) {
        $param = { %$param, %{ $_[0] } };
        shift @_;
      }
    
      my @command = @_;
      return unless @command;
    
      #remark( 'debug', 'command run', { command => "@command" } );
    
      my $timeout = $param->{timeout} || 60;
    
      my @results;
    
      eval {
        local $SIG{ALRM} = sub { die "alarm\n" };
    
        alarm $timeout;
    
        my $output_fh;
        open $output_fh, '-|' or exec @command or die "exec error\n";
        @results = <$output_fh>;
        close $output_fh;
    
        alarm 0;
      };
      if ($@) {
        my $error_str =
            $@ eq "alarm\n"      ? 'command timed out'
          : $@ eq "exec error\n" ? undef
          :                        'unexpected command error';
    
        if ( defined $error_str ) {
          chomp $@;
          remark( 'error', $error_str, { command => "@command", errno => $@ } );
        }
      }
    
      return @results ? \@results : undef;
    }
    
    sub duration2seconds {
      my $tmpdur = shift;
      my $seconds;
    
      # how to convert short human durations into seconds
      my %factor = (
        y => 31536000,
        w => 604800,
        d => 86400,
        h => 3600,
        m => 60,
        s => 1,
      );
    
      # assume raw seconds for plain number
      if ( $tmpdur =~ m/^\d+$/ ) {
        $seconds = $tmpdur * 60;
    
      } elsif ( $tmpdur =~ m/^[ywdhms\d\s]+$/ ) {
    
        # match "2m 5s" style input and convert to seconds
        while ( $tmpdur =~ m/(\d+)\s*([ywdhms])/g ) {
          $seconds += $1 * $factor{$2};
        }
      } else {
        remark( 'error', 'unknown characters in duration', { data => $tmpdur } );
        do_exit(78);
      }
    
      unless ( defined $seconds and $seconds =~ m/^\d+$/ ) {
        remark( 'error', 'unable to parse duration', { data => $tmpdur } );
        do_exit(78);
      }
    
      return $seconds;
    }
    
    # Wrapper for exit values, in case need to alter them under particular
    # monitoring systems. Script uses 100+ for various error conditions.
    sub do_exit {
      my $exit_value = shift;
    
      exit $exit_value;
    }
    
    sub remark {
      my $priority   = shift;
      my $message    = shift;
      my $attributes = shift;
    
      chomp $message;
    
      my $attr_str;
      if ($attributes) {
        $attr_str = join ', ',
          map { $attributes->{$_} ||= q{}; "$_=$attributes->{$_}" }
          sort keys %$attributes;
      }
    
      print STDERR "$priority: $message"
        . ( $attr_str ? ": $attr_str" : q{} ) . "\n";
      return 1;
    }
    
    sub print_help {
      print <<"END_USAGE";
    Usage: $basename -f prefs [options] mode [arguments]
    
    Expiration date handler.
    
    Options:
      -h/-?  Display this message.
    
      -f pp  Read preferences file from pp.
    
      -c cc  Specify custom class to read from preferences file.
    
      -l     List supported modes and exit.
    
    Run perldoc(1) on this script for additional documentation.
    
    END_USAGE
      return;
    }
    
    __END__
    
    =head1 NAME
    
    check-expire - expiration date checker and handler
    
    =head1 SYNOPSIS
    
    Check the certificate on C<www.example.org:443>, and handle the expired
    or near-expired certificate via options in the C <prefs>configuration
    file:
    
      $ check-expire -f prefs  certificate  check-web www.example.org:443
    
    Where the C <check-web>wrapper outputs the certificate via:
    
      #!/bin/sh
      echo GET / HTTP/1.0 | openssl s_client -connect "$1" 2>&1
    
    Directly accessible certificate files can be checked via:
    
      $ check-expire -f prefs  openssl-x509 -in /path/to/some.cert
    
    =head1 DESCRIPTION
    
    =head2 Overview
    
    Checks expiration dates on data like x509 certificates. Uses preferences
    to handle expired certificates, or configurable actions should the
    expiration date be inside a particular time window. Requires wrapper
    scripts to obtain the certificate or expiration date to parse.
    
    =head2 Normal Usage
    
      $ check-expire -f prefs  mode  [mode options]
    
    See L<"OPTIONS"> for details on the command line switches supported.
    
    =head1 OPTIONS
    
    This script currently supports the following command line switches.
    Arguments following the C <mode>will vary.
    
    =over 4
    
    =item B<-h>, B<-?>
    
    Prints a brief usage note about the script.
    
    =item B<-f> I <prefs>Read handling preferences from the I <prefs>file. See L<"FILES"> for
    configuration details.
    
    =item B<-c> I <class>Specify a custom handling class, otherwise set to C<default>. The class
    is read from the preferences file, see L<"FILES"> for details.
    
    =item B<-l>
    
    List allowed modes to standard output and exit.
    
    =back
    
    =head1 FILES
    
    The preferences file specifies how to handle expiration dates. See below
    for an example. At minimum, a named C <class>block should be created
    with the name C<default>. This block should contain rules to handle
    different conditions: C <expired>will be called if the data is expired,
    followed by any C <window>blocks, ordered to process the shortest
    durations first. If none match, a C <default>hander is called.
    
    The first matching handler block will exit the script, with an
    C <exit_value>of zero, unless a different C <exit_value>is set.
    
    Blocks can include C <stdout>to print output, C <exec>to run named
    commands, C <pipe>to send input to a named command, and C <exit_value>to
    change the exit status of C<check-expire>. Aruguments to these options
    can template information about the certificate and other data (see the
    example below for the syntax, and the source for available parameters).
    
    C <exec>and C <pipe>must be commands to run, not shell statements. If
    shell code is needed, write a wrapper script, and execute that.
    
    C <window>handlers must include a single C <inside>statement followed by
    a duration in sections, or a shorthand duration such as C<7d>.
    
    The following outputs values read by SiteScope. For expired or near-
    expired data, errors are raised in SiteScope via the higher return
    codes. Data expiring inside a month generates an e-mail, but no
    SiteScope error.
    
      <class default=""># default handler used if nothing else matches
        <default>stdout "Return Code: 0"</default> 
    
        # higher return code if expiring inside 7 days, e-mail warnings
        <window>inside 7d
    
          stdout "Return Code: 1"
          exit_value 1
    
          pipe expiring soon: %{extra.subject} | /bin/mail -s "cert warning" root</window> 
    
        # just warn via e-mail if expiring inside 30 days
        <window>inside 30d
    
          stdout "Return Code: 0"
          pipe expiring soon: %{extra.subject} | /bin/mail -s "cert warning" root</window> 
    
        # handler for expired data (equal to or past expiration date)
        <expired>exec /usr/bin/logger expired certificate: subject=%{extra.subject}
    
          pipe <<end_pipe<br>Expired certificate:
          subject=%{extra.subject}
          expired=%{time.expire_epoch}
          command=%{program.name} %{program.argv}
        | /bin/mail -s "expired cert" root
      END_PIPE
    
          stdout "Return Code: 2"
          exit_value 2</end_pipe<br></expired></class> 
    
    See L <config::general|config::general>for details on the configuration
    file format.
    
    =head1 BUGS
    
    =head2 Reporting Bugs
    
    Newer versions of this script may be available from:
    
    http://github.com/thrig/sial.org-scripts/tree/master
    
    If the bug is in the latest version, send a report to the author.
    Patches that fix problems or add new features are welcome.
    
    =head2 Known Issues
    
    No known issues.
    
    =head1 TODO
    
    Mode handler for key=value output from a program, to facilitate
    interface scripts to other arbitrary systems.
    
    More metadata in %lookup (with modifying %lookup too much once doing
    tests!), for humanized dates, time durations, and other details.
    
    Document usage under Nagios or other interfaces.
    
    Read things to check from preferences file and loop over, instead of
    needing a different command run for each thing to check?
    
    =head1 SEE ALSO
    
    perl(1), s_client(1), x509(1)
    
    =head1 AUTHOR
    
    Jeremy Mates
    
    =head1 COPYRIGHT
    
    The author disclaims all copyrights and releases this script into the
    public domain.
    
    =cut</config::general|config::general></inside></window></pipe></exec></check-expire></exit_value></pipe></exec></stdout></exit_value></exit_value></default></window></expired></default></class></default></class></prefs></prefs></mode></check-web></prefs></www.example.org:443> 
    

    check-expire.cfg

    
     <class default=""><expired>exit_value 2</expired> 
    
            <window>inside 2w
                    exit_value 1</window></class> 
    
    

    check-certificate-expire

    
    #!/bin/sh
    
    openssl_get_CN_from_file() {
        if [ ! -f "$1" ]; then
          echo "Unable to find file: $1"
          return 1
        fi
    
        X509_CN=`openssl x509 -in "$1" -noout -subject | perl -pi -e 's/^.*\/?CN=([^\/]*).*$/\1/'`
        echo $X509_CN
    }
    
    openssl_get_SN_from_file() {
    
        if [ ! -f "$1" ]; then
            echo "Unable to find file: $1"
            return 1
        fi
    
        X509_SN=`openssl x509 -in "$1" -noout -serial`
        echo $X509_SN
    }
    
    if [ ! -f "$1" ]; then
        echo "Unable to find file: $1"
        exit 1
    fi
    
    /usr/local/OpenSSL/check/check-expire -f /usr/local/OpenSSL/check/check-expire.cfg openssl_x509 -in "$1"
    
    case $? in
        1)
            X509_CN=`openssl_get_CN_from_file "$1"`
            X509_SN=`openssl_get_SN_from_file "$1"`
            if [ ! -z "$X509_CN" ]; then
                echo "$X509_CN, $X509_SN:  certificate expires soon"
            else
                echo "$1: certificate expires soon"
            fi
            ;;
        2)
            X509_CN=`openssl_get_CN_from_file "$1"`
            X509_SN=`openssl_get_SN_from_file "$1"`
            if [ ! -z "$X509_CN" ]; then
                echo "$X509_CN, $X509_SN:  certificate expired!"
            else
                echo "$1: certificate expired!"
            fi
            ;;
    esac
    
    

    Thank You


Log in to reply