#! /usr/bin/perl -w

# Copyright (C) 2001-2002 Oliver Ehli <elmy@acm.org>
# Copyright (C) 2001 Mike Schiraldi <raldi@research.netsol.com>
# Copyright (C) 2003 Bjoern Jacke <bjoern@j3e.de>
# Copyright (C) 2015 Kevin J. McCarthy <kevin@8t8.us>
#
#     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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

use strict;
use File::Copy;
use File::Glob ':glob';
use File::Temp qw(tempfile tempdir);

umask 077;

use Time::Local;

# helper routines
sub usage ();
sub mutt_Q ($);
sub mycopy ($$);
sub query_label ();
sub mkdir_recursive ($);
sub verify_files_exist (@);
sub create_tempfile (;$);
sub new_cert_structure ();
sub create_cert_chains (@);

# openssl helpers
sub openssl_exec (@);
sub openssl_format ($);
sub openssl_x509_query ($@);
sub openssl_hash ($);
sub openssl_fingerprint ($);
sub openssl_emails ($);
sub openssl_p12_to_pem ($$);
sub openssl_verify ($$);
sub openssl_crl_text($);
sub openssl_trust_flag ($$;$);
sub openssl_parse_pem ($$);
sub openssl_dump_cert ($);
sub openssl_purpose_flag ($$);

# key/certificate management methods
sub cm_list_certs ();
sub cm_add_entry ($$$$$$;$);
sub cm_add_cert ($);
sub cm_add_indexed_cert ($$$);
sub cm_add_key ($$$$$$);
sub cm_modify_entry ($$$;$);
sub cm_find_entry ($$);
sub cm_refresh_index ();

# op handlers
sub handle_init_paths ();
sub handle_change_label ($);
sub handle_add_cert ($);
sub handle_add_pem ($);
sub handle_add_p12 ($);
sub handle_add_chain ($$$);
sub handle_verify_cert($$);
sub handle_remove_pair ($);
sub handle_add_root_cert ($);


my $mutt = $ENV{MUTT_CMDLINE} || 'mutt';
my $opensslbin = "/usr/bin/openssl";
my $tmpdir;

# Get the directories mutt uses for certificate/key storage.

my $private_keys_path = mutt_Q 'smime_keys';
die "smime_keys is not set in mutt's configuration file"
   if length $private_keys_path == 0;

my $certificates_path = mutt_Q 'smime_certificates';
die "smime_certificates is not set in mutt's configuration file"
   if length $certificates_path == 0;

my $root_certs_path   = mutt_Q 'smime_ca_location';
die "smime_ca_location is not set in mutt's configuration file"
   if length $root_certs_path == 0;

my $root_certs_switch;
if ( -d $root_certs_path) {
  $root_certs_switch = -CApath;
} else {
  $root_certs_switch = -CAfile;
}


######
# OPS
######

if (@ARGV == 1 and $ARGV[0] eq "init") {
  handle_init_paths();
}
elsif (@ARGV == 1 and $ARGV[0] eq "refresh") {
  cm_refresh_index();
}
elsif (@ARGV == 1 and $ARGV[0] eq "list") {
  cm_list_certs();
}
elsif (@ARGV == 2 and $ARGV[0] eq "label") {
  handle_change_label($ARGV[1]);
}
elsif (@ARGV == 2 and $ARGV[0] eq "add_cert") {
  verify_files_exist($ARGV[1]);
  handle_add_cert($ARGV[1]);
}
elsif (@ARGV == 2 and $ARGV[0] eq "add_pem") {
  verify_files_exist($ARGV[1]);
  handle_add_pem($ARGV[1]);
}
elsif ( @ARGV == 2 and $ARGV[0] eq "add_p12") {
  verify_files_exist($ARGV[1]);
  handle_add_p12($ARGV[1]);
}
elsif (@ARGV == 4 and $ARGV[0] eq "add_chain") {
  verify_files_exist($ARGV[1], $ARGV[2], $ARGV[3]);
  handle_add_chain($ARGV[1], $ARGV[2], $ARGV[3]);
}
elsif ((@ARGV == 2 or @ARGV == 3) and $ARGV[0] eq "verify") {
  verify_files_exist($ARGV[2]) if (@ARGV == 3);
  handle_verify_cert($ARGV[1], $ARGV[2]);
}
elsif (@ARGV == 2 and $ARGV[0] eq "remove") {
  handle_remove_pair($ARGV[1]);
}
elsif (@ARGV == 2 and $ARGV[0] eq "add_root") {
  verify_files_exist($ARGV[1]);
  handle_add_root_cert($ARGV[1]);
}
else {
  usage();
  exit(1);
}

exit(0);


##############  sub-routines  ########################


###################
#  helper routines
###################

sub usage () {
    print <<EOF;

Usage: smime_keys <operation>  [file(s) | keyID [file(s)]]

        with operation being one of:

        init      : no files needed, inits directory structure.
        refresh   : refreshes certificate and key index files.
                    Updates trust flag (expiration).
                    Adds purpose flag if missing.

        list      : lists the certificates stored in database.
        label     : keyID required. changes/removes/adds label.
        remove    : keyID required.
        verify    : 1=keyID and optionally 2=CRL
                    Verifies the certificate chain, and optionally whether
                    this certificate is included in supplied CRL (PEM format).
                    Note: to verify all certificates at the same time,
                    replace keyID with "all"

        add_cert  : certificate required.
        add_chain : three files reqd: 1=Key, 2=certificate
                    plus 3=intermediate certificate(s).
        add_p12   : one file reqd. Adds keypair to database.
                    file is PKCS12 (e.g. export from netscape).
        add_pem   : one file reqd. Adds keypair to database.
                    (file was converted from e.g. PKCS12).

        add_root  : one file reqd. Adds PEM root certificate to the location
                    specified within muttrc (smime_verify_* command)

EOF
}

sub mutt_Q ($) {
  my ($var) = @_;

  my $cmd = "$mutt -v >/dev/null 2>/dev/null";
  system ($cmd) == 0 or die<<EOF;
Couldn't launch mutt. I attempted to do so by running the command "$mutt".
If that's not the right command, you can override it by setting the
environment variable \$MUTT_CMDLINE
EOF

  $cmd = "$mutt -Q $var 2>/dev/null";
  my $answer = `$cmd`;

  $? and die<<EOF;
Couldn't look up the value of the mutt variable "$var".
You must set this in your mutt config file. See contrib/smime.rc for an example.
EOF

  $answer =~ /\"(.*?)\"/ and return bsd_glob($1, GLOB_TILDE | GLOB_NOCHECK);

  $answer =~ /^Mutt (.*?) / and die<<EOF;
This script requires mutt 1.5.0 or later. You are using mutt $1.
EOF

  die "Value of $var is weird\n";
}

sub mycopy ($$) {
  my ($source, $dest) = @_;

  copy $source, $dest or die "Problem copying $source to $dest: $!\n";
}

sub query_label () {
  my $input;
  my $label;
  my $junk;

  print "\nYou may assign a label to this key, so you don't have to remember\n";
  print "the key ID. This has to be _one_ word (no whitespaces).\n\n";

  print "Enter label: ";
  $input = <STDIN>;

  if (defined($input) && ($input !~ /^\s*$/)) {
    chomp($input);
    $input =~ s/^\s+//;
    ($label, $junk) = split(/\s/, $input, 2);

    if (defined($junk)) {
      print "\nUsing '$label' as label; ignoring '$junk'\n";
    }
  }

  if ((! defined($label)) || ($label =~ /^\s*$/)) {
    $label =  "-";
  }

  return $label;
}

sub mkdir_recursive ($) {
  my ($path) = @_;
  my $tmp_path;

  for my $dir (split /\//, $path) {
    $tmp_path .= "$dir/";

    -d $tmp_path
      or mkdir $tmp_path, 0700
        or die "Can't mkdir $tmp_path: $!";
  }
}

sub verify_files_exist (@) {
  my (@files) = @_;

  foreach my $file (@files) {
    if ((! -e $file) || (! -s $file)) {
      die("$file is nonexistent or empty.");
    }
  }
}

# Returns a list ($fh, $filename)
sub create_tempfile (;$) {
  my ($directory) = @_;

  if (! defined($directory)) {
    if (! defined($tmpdir)) {
      $tmpdir = tempdir(CLEANUP => 1);
    }
    $directory = $tmpdir;
  }

  return tempfile(DIR => $directory);
}

# Creates a cert data structure used by openssl_parse_pem
sub new_cert_structure () {
  my $cert_data = {};

  $cert_data->{datafile} = "";
  $cert_data->{type} = "";
  $cert_data->{localKeyID} = "";
  $cert_data->{subject} = "";
  $cert_data->{issuer} = "";

  return $cert_data;
}

sub create_cert_chains (@) {
  my (@certs) = @_;

  my (%subject_hash, @leaves, @chains);

  foreach my $cert (@certs) {
    $cert->{children} = 0;
    if ($cert->{subject}) {
      $subject_hash{$cert->{subject}} = $cert;
    }
  }

  foreach my $cert (@certs) {
    my $parent = $subject_hash{$cert->{issuer}};
    if (defined($parent)) {
      $parent->{children} += 1;
    }
  }

  @leaves = grep { $_->{children} == 0 } @certs;
  foreach my $leaf (@leaves) {
    my $chain = [];
    my $cert = $leaf;

    while (defined($cert)) {
      push @$chain, $cert;

      $cert = $subject_hash{$cert->{issuer}};
      if (defined($cert) &&
          (scalar(grep {$_ == $cert} @$chain) != 0)) {
        $cert = undef;
      }
    }

    push @chains, $chain;
  }

  return @chains;
}


##################
# openssl helpers
##################

sub openssl_exec (@) {
  my (@args) = @_;

  my $fh;

  open($fh, "-|", $opensslbin, @args)
    or die "Failed to run '$opensslbin @args': $!";
  my @output = <$fh>;
  if (! close($fh)) {
    # NOTE: Callers should check the value of $? for the exit status.
    if ($!) {
      die "Syserr closing '$opensslbin @args' pipe: $!";
    }
  }

  return @output;
}

sub openssl_format ($) {
  my ($filename) = @_;

  return -B $filename ? 'DER' : 'PEM';
}

sub openssl_x509_query ($@) {
  my ($filename, @query) = @_;

  my $format = openssl_format($filename);
  my @args = ("x509", "-in", $filename, "-inform", $format, "-noout", @query);
  return openssl_exec(@args);
}

sub openssl_hash ($) {
  my ($filename) = @_;

  my $cert_hash = join("", openssl_x509_query($filename, "-hash"));
  $? and die "openssl -hash '$filename' returned $?";

  chomp($cert_hash);
  return $cert_hash;
}

sub openssl_fingerprint ($) {
  my ($filename) = @_;

  my $fingerprint = join("", openssl_x509_query($filename, "-fingerprint"));
  $? and die "openssl -fingerprint '$filename' returned $?";

  chomp($fingerprint);
  return $fingerprint;
}

sub openssl_emails ($) {
  my ($filename) = @_;

  my @mailboxes = openssl_x509_query($filename, "-email");
  $? and die "openssl -email '$filename' returned $?";

  chomp(@mailboxes);
  return @mailboxes;
}

sub openssl_p12_to_pem ($$) {
  my ($p12_file, $pem_file) = @_;

  my @args = ("pkcs12", "-in", $p12_file, "-out", $pem_file);
  openssl_exec(@args);
  $? and die "openssl pkcs12 conversion returned $?";
}

sub openssl_verify ($$) {
  my ($issuer_path, $cert_path) = @_;

  my @args = ("verify", $root_certs_switch, $root_certs_path,
              "-untrusted", $issuer_path, $cert_path);
  my $output = join("", openssl_exec(@args));

  chomp($output);
  return $output;
}

sub openssl_crl_text($) {
  my ($crl) = @_;

  my @args = ("crl", "-text", "-noout", "-in", $crl);
  my @output = openssl_exec(@args);
  $? and die "openssl crl -text '$crl' returned $?";

  return @output;
}

sub openssl_trust_flag ($$;$) {
  my ($cert, $issuerid, $crl) = @_;

  print "==> about to verify certificate of $cert\n";

  my $result = 't';
  my $issuer_path;
  my $cert_path = "$certificates_path/$cert";

  if ($issuerid eq '?') {
    $issuer_path = "$certificates_path/$cert";
  } else {
    $issuer_path = "$certificates_path/$issuerid";
  }

  my $output = openssl_verify($issuer_path, $cert_path);
  if ($?) {
    print "openssl verify returned exit code " . ($? >> 8) . " with output:\n";
    print "$output\n\n";
    print "Marking certificate as invalid\n";
    return 'i';
  }
  print "\n$output\n";

  if ($output !~ /OK/) {
    return 'i';
  }

  my ($not_before, $not_after, $serial_in) = openssl_x509_query($cert_path, "-dates", "-serial");
  $? and die "openssl -dates -serial '$cert_path' returned $?";

  if ( defined $not_before and defined $not_after ) {
    my %months = ('Jan', '00', 'Feb', '01', 'Mar', '02', 'Apr', '03',
                  'May', '04', 'Jun', '05', 'Jul', '06', 'Aug', '07',
                  'Sep', '08', 'Oct', '09', 'Nov', '10', 'Dec', '11');

    my @tmp = split (/\=/, $not_before);
    my $not_before_date = $tmp[1];
    my @fields =
      $not_before_date =~ /(\w+)\s*(\d+)\s*(\d+):(\d+):(\d+)\s*(\d+)\s*GMT/;
    if ($#fields == 5) {
      if (timegm($fields[4], $fields[3], $fields[2], $fields[1],
                 $months{$fields[0                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          