Navigation

    Netgate Discussion Forum
    • Register
    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Search

    Certificate Authority Manager

    General pfSense Questions
    1
    1
    771
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • A
      allxi last edited by

      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

      1 Reply Last reply Reply Quote 0
      • First post
        Last post