From 8e76cfdbd4b39679f22e21c9417889fcd244578b Mon Sep 17 00:00:00 2001 From: Kevin McCarthy Date: Fri, 15 May 2015 10:47:38 -0700 Subject: [PATCH] Start cleaning up and fixing smime_keys.pl (closes #3324) (see #2456) * Convert to using File::Temp (#3324). This was also suggested at https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=775199 * Use File::Temp for add_p12 temp file. (#2456) * Make the query_label() method a bit more robust with empty strings, ctrl-d, and leading spaces. * Clean up openssl_do_verify() logic. Mark cert as invalid rather that die'ing if an openssl verify command fails. * General cleanup: - Clearly separate op handler, certificate management, and helper functions by section and using prefixes. - Create openssl helper functions to reduce copy/paste invocations and make the code clearer. - Make indentation consistent at 2 spaces. - Change handle_add_pem() to re-use handle_add_chain() once the correct files are identified. - Change openssl_parse_pem() to return a single array of data structures representing the parsed certs/keys. --- smime_keys.pl | 1249 ++++++++++++++++++++++++------------------------- 1 file changed, 621 insertions(+), 628 deletions(-) diff --git a/smime_keys.pl b/smime_keys.pl index a82535883..7001bdddf 100755 --- a/smime_keys.pl +++ b/smime_keys.pl @@ -8,12 +8,12 @@ # 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. @@ -21,158 +21,129 @@ 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 newfile ($;$$); -sub mutt_Q ($ ); +sub mutt_Q ($); sub mycopy ($$); - -# directory setup routines -sub mkdir_recursive ($ ); -sub init_paths (); +sub query_label (); +sub mkdir_recursive ($); +sub verify_files_exist (@); +sub create_tempfile (;$); +sub new_cert_structure (); + +# openssl helpers +sub openssl_format ($); +sub openssl_hash ($); +sub openssl_fingerprint ($); +sub openssl_emails ($); +sub openssl_do_verify($$$); +sub openssl_parse_pem ($$); # key/certificate management methods -sub list_certs (); -sub query_label (); -sub add_entry ($$$$$ ); -sub add_certificate ($$$$;$ ); -sub add_key ($$$$); -sub add_root_cert ($ ); -sub parse_pem (@ ); -sub handle_pem (@ ); -sub modify_entry ($$$;$ ); -sub remove_pair ($ ); -sub change_label ($ ); -sub verify_cert($$); -sub do_verify($$$ ); - -# Get the directories mutt uses for certificate/key storage. +sub cm_list_certs (); +sub cm_add_entry ($$$$$); +sub cm_add_certificate ($$$$;$); +sub cm_add_key ($$$$); +sub cm_modify_entry ($$$;$); + +# 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 @tempfiles = (); -my @cert_tmp_file = (); - 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; + 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; + 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; + if length $root_certs_path == 0; my $root_certs_switch; if ( -d $root_certs_path) { - $root_certs_switch = -CApath; + $root_certs_switch = -CApath; } else { - $root_certs_switch = -CAfile; + $root_certs_switch = -CAfile; } -# +###### # OPS -# +###### if(@ARGV == 1 and $ARGV[0] eq "init") { - init_paths; + handle_init_paths(); } elsif(@ARGV == 1 and $ARGV[0] eq "list") { - list_certs; + cm_list_certs(); } elsif(@ARGV == 2 and $ARGV[0] eq "label") { - change_label($ARGV[1]); + handle_change_label($ARGV[1]); } elsif(@ARGV == 2 and $ARGV[0] eq "add_cert") { - my $format = -B $ARGV[1] ? 'DER' : 'PEM'; - my $cmd = "$opensslbin x509 -noout -hash -in $ARGV[1] -inform $format"; - my $cert_hash = `$cmd`; - $? and die "'$cmd' returned $?"; - chomp($cert_hash); - my $label = query_label; - &add_certificate($ARGV[1], \$cert_hash, 1, $label, '?'); + verify_files_exist($ARGV[1]); + handle_add_cert($ARGV[1]); } elsif(@ARGV == 2 and $ARGV[0] eq "add_pem") { - -e $ARGV[1] and -s $ARGV[1] or die("$ARGV[1] is nonexistent or empty."); - open(PEM_FILE, "<$ARGV[1]") or die("Can't open $ARGV[1]: $!"); - my @pem = ; - close(PEM_FILE); - handle_pem(@pem); + verify_files_exist($ARGV[1]); + handle_add_pem($ARGV[1]); } elsif( @ARGV == 2 and $ARGV[0] eq "add_p12") { - -e $ARGV[1] and -s $ARGV[1] or die("$ARGV[1] is nonexistent or empty."); - - print "\nNOTE: This will ask you for two passphrases:\n"; - print " 1. The passphrase you used for exporting\n"; - print " 2. The passphrase you wish to secure your private key with.\n\n"; - - my $pem_file = "$ARGV[1].pem"; - - my $cmd = "$opensslbin pkcs12 -in $ARGV[1] -out $pem_file"; - system $cmd and die "'$cmd' returned $?"; - - -e $pem_file and -s $pem_file or die("Conversion of $ARGV[1] failed."); - open(PEM_FILE, $pem_file) or die("Can't open $pem_file: $!"); - my @pem = ; - close(PEM_FILE); - unlink $pem_file; - handle_pem(@pem); + verify_files_exist($ARGV[1]); + handle_add_p12($ARGV[1]); } elsif(@ARGV == 4 and $ARGV[0] eq "add_chain") { - my $mailbox; - my $format = -B $ARGV[2] ? 'DER' : 'PEM'; - my $cmd = "$opensslbin x509 -noout -hash -in $ARGV[2] -inform $format"; - my $cert_hash = `$cmd`; - - $? and die "'$cmd' returned $?"; - - $format = -B $ARGV[3] ? 'DER' : 'PEM'; - - $cmd = "$opensslbin x509 -noout -hash -in $ARGV[3] -inform $format"; - my $issuer_hash = `$cmd`; - $? and die "'$cmd' returned $?"; - - chomp($cert_hash); - chomp($issuer_hash); - - my $label = query_label; - - add_certificate($ARGV[3], \$issuer_hash, 0, $label); - my @mailbox = &add_certificate($ARGV[2], \$cert_hash, 1, $label, $issuer_hash); - - foreach $mailbox (@mailbox) { - chomp($mailbox); - add_key($ARGV[1], $cert_hash, $mailbox, $label); - } + 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_cert($ARGV[1], $ARGV[2]); + verify_files_exist($ARGV[2]) if (@ARGV == 3); + handle_verify_cert($ARGV[1], $ARGV[2]); } elsif(@ARGV == 2 and $ARGV[0] eq "remove") { - remove_pair($ARGV[1]); + handle_remove_pair($ARGV[1]); } elsif(@ARGV == 2 and $ARGV[0] eq "add_root") { - add_root_cert($ARGV[1]); + verify_files_exist($ARGV[1]); + handle_add_root_cert($ARGV[1]); } -else { - usage; - exit(1); +else { + usage(); + exit(1); } exit(0); +############## sub-routines ######################## - -############## sub-routines ######################## +################### +# helper routines +################### sub usage () { print <; + 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 = shift or die; - 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: $!"; + 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."); } + } } -sub init_paths () { - mkdir_recursive($certificates_path); - mkdir_recursive($private_keys_path); +# Returns a list ($fh, $filename) +sub create_tempfile (;$) { + my ($directory) = @_; + + if (! defined($directory)) { + if (! defined($tmpdir)) { + $tmpdir = tempdir(CLEANUP => 1); + } + $directory = $tmpdir; + } - my $file; + return tempfile(DIR => $directory); +} - $file = $certificates_path . "/.index"; - -f $file or open(TMP_FILE, ">$file") and close(TMP_FILE) - or die "Can't touch $file: $!"; +# Creates a cert data structure used by openssl_parse_pem +sub new_cert_structure () { + my $cert_data = {}; - $file = $private_keys_path . "/.index"; - -f $file or open(TMP_FILE, ">$file") and close(TMP_FILE) - or die "Can't touch $file: $!"; + $cert_data->{datafile} = ""; + $cert_data->{type} = ""; + $cert_data->{localKeyID} = ""; + $cert_data->{subject} = ""; + $cert_data->{issuer} = ""; + + return $cert_data; } +################## +# openssl helpers +################## -# +sub openssl_format ($) { + my ($filename) = @_; + + return -B $filename ? 'DER' : 'PEM'; +} + +sub openssl_hash ($) { + my ($filename) = @_; + + my $format = openssl_format($filename); + my $cmd = "$opensslbin x509 -noout -hash -in $filename -inform $format"; + my $cert_hash = `$cmd`; + $? and die "'$cmd' returned $?"; + + chomp($cert_hash); + return $cert_hash; +} + +sub openssl_fingerprint ($) { + my ($filename) = @_; + + my $format = openssl_format($filename); + my $cmd = "$opensslbin x509 -in $filename -inform $format -fingerprint -noout"; + my $fingerprint = `$cmd`; + $? and die "'$cmd' returned $?"; + + chomp($fingerprint); + return $fingerprint; +} + +sub openssl_emails ($) { + my ($filename) = @_; + + my $format = openssl_format($filename); + my $cmd = "$opensslbin x509 -in $filename -inform $format -email -noout"; + my @mailboxes = `$cmd`; + $? and die "'$cmd' returned $?"; + + chomp(@mailboxes); + return @mailboxes; +} + +sub openssl_do_verify ($$$) { + my ($cert, $issuerid, $crl) = @_; + + 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 $cmd = "$opensslbin verify $root_certs_switch $root_certs_path -purpose smimesign -purpose smimeencrypt -untrusted $issuer_path $cert_path"; + my $output = `$cmd`; + chomp $output; + if ($?) { + print "'$cmd' returned exit code " . ($? >> 8) . " with output:\n"; + print "$output\n\n"; + print "Marking as invalid\n"; + return 'i'; + } + print "\n$output\n"; + + if ($output !~ /OK/) { + return 'i'; + } + + my $format = openssl_format($cert_path); + $cmd = "$opensslbin x509 -dates -serial -noout -in $cert_path -inform $format"; + (my $not_before, my $not_after, my $serial_in) = `$cmd`; + $? and die "'$cmd' 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]}, $fields[5]) > time) { + print "Certificate is not yet valid.\n"; + return 'e'; + } + } else { + print "Expiration Date: Parse Error : $not_before_date\n\n"; + } + + @tmp = split (/\=/, $not_after); + my $not_after_date = $tmp[1]; + @fields = + $not_after_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]}, $fields[5]) < time) { + print "Certificate has expired.\n"; + return 'e'; + } + } else { + print "Expiration Date: Parse Error : $not_after_date\n\n"; + } + } + + if ( defined $crl ) { + my @serial = split (/\=/, $serial_in); + my $cmd = "$opensslbin crl -text -noout -in $crl | grep -A1 $serial[1]"; + (my $l1, my $l2) = `$cmd`; + $? and die "'$cmd' returned $?"; + + if ( defined $l2 ) { + my @revoke_date = split (/:\s/, $l2); + print "FAILURE: Certificate $cert has been revoked on $revoke_date[1]\n"; + $result = 'r'; + } + } + print "\n"; + + return $result; +} + +sub openssl_parse_pem ($$) { + my ($filename, $attrs_required) = @_; + + my $state = 0; + my $cert_data; + my @certs; + my $cert_count = 0; + my $bag_count = 0; + my $cert_tmp_fh; + my $cert_tmp_filename; + + $cert_data = new_cert_structure(); + ($cert_tmp_fh, $cert_data->{datafile}) = create_tempfile(); + + open(PEM_FILE, "<$filename") or die("Can't open $filename: $!"); + while() { + if(/^Bag Attributes/) { + $bag_count++; + $state == 0 or die("PEM-parse error at: $."); + $state = 1; + } + + if ($state == 1) { + if (/localKeyID:\s*(.*)/) { + $cert_data->{localKeyID} = $1; + } + + if (/subject=\s*(.*)/) { + $cert_data->{subject} = $1; + } + + if (/issuer=\s*(.*)/) { + $cert_data->{issuer} = $1; + } + } + + + if(/^-----/) { + if(/BEGIN/) { + print $cert_tmp_fh $_; + $state = 2; + + if(/PRIVATE/) { + $cert_data->{type} = "K"; + next; + } + if(/CERTIFICATE/) { + $cert_data->{type} = "C"; + next; + } + die("What's this: $_"); + } + if(/END/) { + $state = 0; + print $cert_tmp_fh $_; + close($cert_tmp_fh); + + $cert_count++; + push (@certs, $cert_data); + + $cert_data = new_cert_structure(); + ($cert_tmp_fh, $cert_data->{datafile}) = create_tempfile(); + next; + } + } + print $cert_tmp_fh $_; + } + close($cert_tmp_fh); + close(PEM_FILE); + + if ($attrs_required && ($bag_count != $cert_count)) { + die("Not all contents were bagged. can't continue."); + } + + return @certs; +} + + +################################# # certificate management methods -# +################################# -sub list_certs () { +sub cm_list_certs () { my %keyflags = ( 'i', '(Invalid)', 'r', '(Revoked)', 'e', '(Expired)', - 'u', '(Unverified)', 'v', '(Valid)', 't', '(Trusted)'); + 'u', '(Unverified)', 'v', '(Valid)', 't', '(Trusted)'); - open(INDEX, "<$certificates_path/.index") or + open(INDEX, "<$certificates_path/.index") or die "Couldn't open $certificates_path/.index: $!"; - + print "\n"; while() { my $tmp; @@ -308,7 +528,7 @@ sub list_certs () { die "Couldn't open $certfile: $!"; local $/; $cert = ; - close F; + close F; } my $subject_in; @@ -316,7 +536,7 @@ sub list_certs () { my $date1_in; my $date2_in; - my $format = -B $certfile ? 'DER' : 'PEM'; + my $format = openssl_format($certfile); my $cmd = "$opensslbin x509 -subject -issuer -dates -noout -in $certfile -inform $format"; ($subject_in, $issuer_in, $date1_in, $date2_in) = `$cmd`; $? and print "ERROR: '$cmd' returned $?\n\n" and next; @@ -345,13 +565,13 @@ sub list_certs () { $tmp = $tmp[1]; @tmp = split (/\=/, $date2_in); print $tab."Certificate is not valid before $tmp". - $tab." or after ".$tmp[1]; + $tab." or after ".$tmp[1]; } -e "$private_keys_path/$fields[1]" and print "$tab - Matching private key installed -\n"; - $format = -B "$certificates_path/$fields[1]" ? 'DER' : 'PEM'; + $format = openssl_format("$certificates_path/$fields[1]"); $cmd = "$opensslbin x509 -purpose -noout -in $certfile -inform $format"; my $purpose_in = `$cmd`; $? and die "'$cmd' returned $?"; @@ -367,428 +587,175 @@ sub list_certs () { print "\n"; } - - close(INDEX); -} - - -sub query_label () { - my @words; - my $input; - - 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: "; - chomp($input = ); - - my ($label, $junk) = split(/\s/, $input, 2); - - defined $junk - and print "\nUsing '$label' as label; ignoring '$junk'\n"; - - defined $label || ($label = "-"); - - return $label; + close(INDEX); } +sub cm_add_entry ($$$$$) { + my ($mailbox, $hashvalue, $use_cert, $label, $issuer_hash) = @_; + my @fields; -sub add_entry ($$$$$) { - my $mailbox = shift or die; - my $hashvalue = shift or die; - my $use_cert = shift; - my $label = shift or die; - my $issuer_hash = shift; - - my @fields; - - if ($use_cert) { - open(INDEX, "+<$certificates_path/.index") or - die "Couldn't open $certificates_path/.index: $!"; - } - else { - open(INDEX, "+<$private_keys_path/.index") or - die "Couldn't open $private_keys_path/.index: $!"; - } + if ($use_cert) { + open(INDEX, "+<$certificates_path/.index") or + die "Couldn't open $certificates_path/.index: $!"; + } + else { + open(INDEX, "+<$private_keys_path/.index") or + die "Couldn't open $private_keys_path/.index: $!"; + } - while() { - @fields = split; - return if ($fields[0] eq $mailbox && $fields[1] eq $hashvalue); - } + while() { + @fields = split; + return if ($fields[0] eq $mailbox && $fields[1] eq $hashvalue); + } - if ($use_cert) { - print INDEX "$mailbox $hashvalue $label $issuer_hash u\n"; - } - else { - print INDEX "$mailbox $hashvalue $label \n"; - } + if ($use_cert) { + print INDEX "$mailbox $hashvalue $label $issuer_hash u\n"; + } + else { + print INDEX "$mailbox $hashvalue $label \n"; + } - close(INDEX); + close(INDEX); } +sub cm_add_certificate ($$$$;$) { + my ($filename, $hashvalue, $add_to_index, $label, $issuer_hash) = @_; -sub add_certificate ($$$$;$) { - my $filename = shift or die; - my $hashvalue = shift or die; - my $add_to_index = shift; - my $label = shift or die; - my $issuer_hash = shift; + my $iter = 0; + my @mailboxes; - my $iter = 0; - my @mailbox; - my $mailbox; + my $fp1 = openssl_fingerprint($filename); - while(-e "$certificates_path/$$hashvalue.$iter") { - my ($t1, $t2); - my $format = -B $filename ? 'DER' : 'PEM'; - my $cmd = "$opensslbin x509 -in $filename -inform $format -fingerprint -noout"; - $t1 = `$cmd`; - $? and die "'$cmd' returned $?"; + while (-e "$certificates_path/$$hashvalue.$iter") { + my $fp2 = openssl_fingerprint("$certificates_path/$$hashvalue.$iter"); - $format = -B "$certificates_path/$$hashvalue.$iter" ? 'DER' : 'PEM'; - $cmd = "$opensslbin x509 -in $certificates_path/$$hashvalue.$iter -inform $format -fingerprint -noout"; - $t2 = `$cmd`; - $? and die "'$cmd' returned $?"; - - $t1 eq $t2 and last; + last if $fp1 eq $fp2; + $iter++; + } + $$hashvalue .= ".$iter"; - $iter++; - } - $$hashvalue .= ".$iter"; - - if (-e "$certificates_path/$$hashvalue") { - print "\nCertificate: $certificates_path/$$hashvalue already installed.\n"; + if (-e "$certificates_path/$$hashvalue") { + print "\nCertificate: $certificates_path/$$hashvalue already installed.\n"; + } + else { + mycopy $filename, "$certificates_path/$$hashvalue"; + + if ($add_to_index) { + @mailboxes = openssl_emails($filename); + foreach my $mailbox (@mailboxes) { + cm_add_entry($mailbox, $$hashvalue, 1, $label, $issuer_hash); + print "\ncertificate $$hashvalue ($label) for $mailbox added.\n"; + } + cm_modify_entry('V', $$hashvalue, 1); } else { - mycopy $filename, "$certificates_path/$$hashvalue"; - - if ($add_to_index) { - my $format = -B $filename ? 'DER' : 'PEM'; - my $cmd = "$opensslbin x509 -in $filename -inform $format -email -noout"; - @mailbox = `$cmd`; - $? and die "'$cmd' returned $?"; - - foreach $mailbox (@mailbox) { - chomp($mailbox); - add_entry($mailbox, $$hashvalue, 1, $label, $issuer_hash); - - print "\ncertificate $$hashvalue ($label) for $mailbox added.\n"; - } - verify_cert($$hashvalue, undef); - } - else { - print "added certificate: $certificates_path/$$hashvalue.\n"; - } + print "added certificate: $certificates_path/$$hashvalue.\n"; } + } - return @mailbox; + return @mailboxes; } - -sub add_key ($$$$) { - my $file = shift or die; - my $hashvalue = shift or die; - my $mailbox = shift or die; - my $label = shift or die; +sub cm_add_key ($$$$) { + my ($file, $hashvalue, $mailbox, $label) = @_; unless (-e "$private_keys_path/$hashvalue") { mycopy $file, "$private_keys_path/$hashvalue"; - } + } - add_entry($mailbox, $hashvalue, 0, $label, ""); + cm_add_entry($mailbox, $hashvalue, 0, $label, ""); print "added private key: " . "$private_keys_path/$hashvalue for $mailbox\n"; -} - - - - - - -sub parse_pem (@) { - my $state = 0; - my $cert_iter = 0; - my @bag_attribs; - my $numBags = 0; - - $cert_tmp_file[$cert_iter] = newfile("cert_tmp.$cert_iter","temp"); - my $cert_tmp_iter = $cert_tmp_file[$cert_iter]; - open(CERT_FILE, ">$cert_tmp_iter") - or die "Couldn't open $cert_tmp_iter: $!"; - - while($_ = shift(@_)) { - if(/^Bag Attributes/) { - $numBags++; - $state == 0 or die("PEM-parse error at: $."); - $state = 1; - $bag_attribs[$cert_iter*4+1] = ""; - $bag_attribs[$cert_iter*4+2] = ""; - $bag_attribs[$cert_iter*4+3] = ""; - } - - ($state == 1) and /localKeyID:\s*(.*)/ - and ($bag_attribs[$cert_iter*4+1] = $1); - - ($state == 1) and /subject=\s*(.*)/ - and ($bag_attribs[$cert_iter*4+2] = $1); - - ($state == 1) and /issuer=\s*(.*)/ - and ($bag_attribs[$cert_iter*4+3] = $1); - - if(/^-----/) { - if(/BEGIN/) { - print CERT_FILE; - $state = 2; - - if(/PRIVATE/) { - $bag_attribs[$cert_iter*4] = "K"; - next; - } - if(/CERTIFICATE/) { - $bag_attribs[$cert_iter*4] = "C"; - next; - } - die("What's this: $_"); - } - if(/END/) { - $state = 0; - print CERT_FILE; - close(CERT_FILE); - $cert_iter++; - $cert_tmp_file[$cert_iter] = newfile("cert_tmp.$cert_iter","temp"); - $cert_tmp_iter = $cert_tmp_file[$cert_iter]; - open(CERT_FILE, ">$cert_tmp_iter") - or die "Couldn't open $cert_tmp_iter: $!"; - next; - } - } - print CERT_FILE; - } - close(CERT_FILE); - - # I'll add support for unbagged cetificates, in case this is needed. - $numBags == $cert_iter or - die("Not all contents were bagged. can't continue."); - - return @bag_attribs; } +sub cm_modify_entry ($$$;$) { + my ($op, $hashvalue, $use_cert, $opt_param) = @_; -# This requires the Bag Attributes to be set -sub handle_pem (@) { - - my @pem_contents; - my $iter=0; - my $root_cert; - my $key; - my $certificate; - my $intermediate; - my @mailbox; - my $mailbox; - - @pem_contents = &parse_pem(@_); + my $crl; + my $label; + my $path; + my @fields; - # private key and certificate use the same 'localKeyID' - while($iter <= $#pem_contents / 4) { - if($pem_contents[$iter * 4] eq "K") { - $key = $iter; - last; - } - $iter++; - } - ($iter > $#pem_contents / 2) and die("Couldn't find private key!"); + $op eq 'L' and ($label = $opt_param); + $op eq 'V' and ($crl = $opt_param); - $pem_contents[($key * 4)+1] or die("Attribute 'localKeyID' wasn't set."); - $iter = 0; - while($iter <= $#pem_contents / 4) { - $iter == $key and ($iter++) and next; - if($pem_contents[($iter * 4)+1] eq $pem_contents[($key * 4)+1]) { - $certificate = $iter; - last; - } - $iter++; - } - ($iter > $#pem_contents / 4) and die("Couldn't find matching certificate!"); + if ($use_cert) { + $path = $certificates_path; + } + else { + $path = $private_keys_path; + } - my $tmp_key = newfile("tmp_key","temp"); - mycopy $cert_tmp_file[$key], $tmp_key; - my $tmp_certificate = newfile("tmp_certificate","temp"); - mycopy $cert_tmp_file[$certificate], $tmp_certificate; + open(INDEX, "<$path/.index") or + die "Couldn't open $path/.index: $!"; + my ($newindex_fh, $newindex) = create_tempfile(); - # root certificate is self signed - $iter = 0; + while() { + @fields = split; + if($fields[1] eq $hashvalue or $hashvalue eq 'all') { + $op eq 'R' and next; - while($iter <= $#pem_contents / 4) { - if ($iter == $key or $iter == $certificate) { - $iter++; - next; - } + print $newindex_fh "$fields[0] $fields[1]"; - if($pem_contents[($iter * 4)+2] eq $pem_contents[($iter * 4)+3]) { - $root_cert = $iter; - last; + if($op eq 'L') { + if($use_cert) { + print $newindex_fh " $label $fields[3] $fields[4]"; } - $iter++; - } - if ($iter > $#pem_contents / 4) { - print "Couldn't identify root certificate!\n"; - $root_cert = -1; - } - - # what's left are intermediate certificates. - $iter = 0; - - # needs to be set, so we can check it later - $intermediate = $root_cert; - my $tmp_issuer_cert = newfile("tmp_issuer_cert","temp"); - while($iter <= $#pem_contents / 4) { - if ($iter == $key or $iter == $certificate or $iter == $root_cert) { - $iter++; - next; + else { + print $newindex_fh " $label"; } + } - open (IC, ">> $tmp_issuer_cert") or die "can't open $tmp_issuer_cert: $?"; - my $cert_tmp_iter = $cert_tmp_file[$iter]; - open (CERT, "< $cert_tmp_iter") or die "can't open $cert_tmp_iter: $?"; - print IC while (); - close IC; - close CERT; - - # although there may be many, just need to know if there was any - $intermediate = $iter; - - $iter++; - } - - # no intermediate certificates ? use root-cert instead (if that was found...) - if($intermediate == $root_cert) { - if ($root_cert == -1) { - die("No root and no intermediate certificates. Can't continue."); - } - mycopy $cert_tmp_file[$root_cert], $tmp_issuer_cert; - } - - my $label = query_label; - - my $format = -B $tmp_certificate ? 'DER' : 'PEM'; - my $cmd = "$opensslbin x509 -noout -hash -in $tmp_certificate -inform $format"; - my $cert_hash = `$cmd`; - $? and die "'$cmd' returned $?"; - - $format = -B $tmp_issuer_cert ? 'DER' : 'PEM'; - $cmd = "$opensslbin x509 -noout -hash -in $tmp_issuer_cert -inform $format"; - my $issuer_hash = `$cmd`; - $? and die "'$cmd' returned $?"; - - chomp($cert_hash); chomp($issuer_hash); - - # Note: $cert_hash will be changed to reflect the correct filename - # within add_cert() ONLY, so these _have_ to get called first.. - add_certificate($tmp_issuer_cert, \$issuer_hash, 0, $label); - @mailbox = &add_certificate("$tmp_certificate", \$cert_hash, 1, $label, $issuer_hash); - foreach $mailbox (@mailbox) { - chomp($mailbox); - add_key($tmp_key, $cert_hash, $mailbox, $label); - } -} - - - - - - -sub modify_entry ($$$;$ ) { - my $op = shift or die; - my $hashvalue = shift or die; - my $use_cert = shift; - my $crl; - my $label; - my $path; - my @fields; - - $op eq 'L' and ($label = shift or die); - $op eq 'V' and ($crl = shift); - - - if ($use_cert) { - $path = $certificates_path; - } - else { - $path = $private_keys_path; - } + if ($op eq 'V') { + print "\n==> about to verify certificate of $fields[0]\n"; + my $flag = openssl_do_verify($fields[1], $fields[3], $crl); + print $newindex_fh " $fields[2] $fields[3] $flag"; + } - open(INDEX, "<$path/.index") or - die "Couldn't open $path/.index: $!"; - my $newindex = newfile("$path/.index.tmp"); - open(NEW_INDEX, ">$newindex") or - die "Couldn't create $newindex: $!"; - - while() { - @fields = split; - if($fields[1] eq $hashvalue or $hashvalue eq 'all') { - $op eq 'R' and next; - print NEW_INDEX "$fields[0] $fields[1]"; - if($op eq 'L') { - if($use_cert) { - print NEW_INDEX " $label $fields[3] $fields[4]"; - } - else { - print NEW_INDEX " $label"; - } - } - if ($op eq 'V') { - print "\n==> about to verify certificate of $fields[0]\n"; - my $flag = &do_verify($fields[1], $fields[3], $crl); - print NEW_INDEX " $fields[2] $fields[3] $flag"; - } - print NEW_INDEX "\n"; - next; - } - print NEW_INDEX; + print $newindex_fh "\n"; + next; } - close(INDEX); - close(NEW_INDEX); + print $newindex_fh $_; + } + close(INDEX); + close($newindex_fh); - rename $newindex, "$path/.index" - or die "Couldn't rename $newindex to $path/.index: $!\n"; + move $newindex, "$path/.index" + or die "Couldn't move $newindex to $path/.index: $!\n"; - print "\n"; + print "\n"; } +############## +# Op handlers +############## +sub handle_init_paths () { + mkdir_recursive($certificates_path); + mkdir_recursive($private_keys_path); -sub remove_pair ($ ) { - my $keyid = shift or die; + my $file; - if (-e "$certificates_path/$keyid") { - unlink "$certificates_path/$keyid"; - modify_entry('R', $keyid, 1); - print "Removed certificate $keyid.\n"; - } - else { - die "No such certificate: $keyid"; - } + $file = $certificates_path . "/.index"; + -f $file or open(TMP_FILE, ">$file") and close(TMP_FILE) + or die "Can't touch $file: $!"; - if (-e "$private_keys_path/$keyid") { - unlink "$private_keys_path/$keyid"; - modify_entry('R', $keyid, 0); - print "Removed private key $keyid.\n"; - } + $file = $private_keys_path . "/.index"; + -f $file or open(TMP_FILE, ">$file") and close(TMP_FILE) + or die "Can't touch $file: $!"; } +sub handle_change_label ($) { + my ($keyid) = @_; - -sub change_label ($ ) { - my $keyid = shift or die; - - my $label = query_label; + my $label = query_label(); if (-e "$certificates_path/$keyid") { - modify_entry('L', $keyid, 1, $label); + cm_modify_entry('L', $keyid, 1, $label); print "Changed label for certificate $keyid.\n"; } else { @@ -796,130 +763,189 @@ sub change_label ($ ) { } if (-e "$private_keys_path/$keyid") { - modify_entry('L', $keyid, 0, $label); + cm_modify_entry('L', $keyid, 0, $label); print "Changed label for private key $keyid.\n"; } - } +sub handle_add_cert($) { + my ($filename) = @_; + my $label = query_label(); + my $cert_hash = openssl_hash($filename); + cm_add_certificate($filename, \$cert_hash, 1, $label, '?'); -sub verify_cert ($$) { - my $keyid = shift or die; - my $crl = shift; + # TODO: + # Below is the method from http://kb.wisc.edu/middleware/page.php?id=4091 + # Investigate threading the chain and separating out issuer as an alternative. - -e "$certificates_path/$keyid" or $keyid eq 'all' - or die "No such certificate: $keyid"; - modify_entry('V', $keyid, 1, $crl); + # my @cert_contents = openssl_parse_pem($filename, 0); + # foreach my $cert (@cert_contents) { + # if ($cert->{type} eq "C") { + # my $cert_hash = openssl_hash($cert->{datafile}); + # cm_add_certificate($cert->{datafile}, \$cert_hash, 1, $label, '?'); + # } else { + # print "Ignoring private key\n"; + # } + # } } +sub handle_add_pem ($) { + my ($filename) = @_; + + my @pem_contents; + my $iter; + my $key; + my $certificate; + my $root_cert; + my $issuer_cert_file; + + @pem_contents = openssl_parse_pem($filename, 1); + + # look for key + $iter = 0; + while($iter <= $#pem_contents) { + if ($pem_contents[$iter]->{type} eq "K") { + $key = $pem_contents[$iter]; + splice(@pem_contents, $iter, 1); + last; + } + $iter++; + } + defined($key) or die("Couldn't find private key!"); + $key->{localKeyID} or die("Attribute 'localKeyID' wasn't set."); + + # private key and certificate use the same 'localKeyID' + $iter = 0; + while($iter <= $#pem_contents) { + if (($pem_contents[$iter]->{type} eq "C") && + ($pem_contents[$iter]->{localKeyID} eq $key->{localKeyID})) { + $certificate = $pem_contents[$iter]; + splice(@pem_contents, $iter, 1); + last; + } + $iter++; + } + defined($certificate) or die("Couldn't find matching certificate!"); + if ($#pem_contents < 0) { + die("No root and no intermediate certificates. Can't continue."); + } + # Look for a self signed root certificate + $iter = 0; + while($iter <= $#pem_contents) { + if ($pem_contents[$iter]->{subject} eq $pem_contents[$iter]->{issuer}) { + $root_cert = $pem_contents[$iter]; + splice(@pem_contents, $iter, 1); + last; + } + $iter++; + } + if (defined($root_cert)) { + $issuer_cert_file = $root_cert->{datafile}; + } else { + print "Couldn't identify root certificate!\n"; + } -sub do_verify($$$) { - - my $cert = shift or die; - my $issuerid = shift or die; - my $crl = shift; + # what's left are intermediate certificates. + if ($#pem_contents >= 0) { + my ($tmp_issuer_cert_fh, $tmp_issuer_cert) = create_tempfile(); + $issuer_cert_file = $tmp_issuer_cert; - my $result = 'i'; - my $trust_q; - my $issuer_path; - my $cert_path = "$certificates_path/$cert"; + $iter = 0; + while($iter <= $#pem_contents) { + my $cert_datafile = $pem_contents[$iter]->{datafile}; + open (CERT, "< $cert_datafile") or die "can't open $cert_datafile: $?"; + print $tmp_issuer_cert_fh $_ while (); + close CERT; - if($issuerid eq '?') { - $issuer_path = "$certificates_path/$cert"; - } else { - $issuer_path = "$certificates_path/$issuerid"; + $iter++; + } + close $tmp_issuer_cert_fh; } - my $cmd = "$opensslbin verify $root_certs_switch $root_certs_path -purpose smimesign -purpose smimeencrypt -untrusted $issuer_path $cert_path"; - my $output = `$cmd`; - $? and die "'$cmd' returned $?"; - chop $output; - print "\n$output\n"; + handle_add_chain($key->{datafile}, $certificate->{datafile}, $issuer_cert_file); +} - ($output =~ /OK/) and ($result = 'v'); +sub handle_add_p12 ($) { + my ($filename) = @_; - $result eq 'i' and return $result; + print "\nNOTE: This will ask you for two passphrases:\n"; + print " 1. The passphrase you used for exporting\n"; + print " 2. The passphrase you wish to secure your private key with.\n\n"; - my $format = -B $cert_path ? 'DER' : 'PEM'; - $cmd = "$opensslbin x509 -dates -serial -noout -in $cert_path -inform $format"; - (my $date1_in, my $date2_in, my $serial_in) = `$cmd`; - $? and die "'$cmd' returned $?"; + my ($pem_fh, $pem_file) = create_tempfile(); + close($pem_fh); - if ( defined $date1_in and defined $date2_in ) { - my @tmp = split (/\=/, $date1_in); - my $tmp = $tmp[1]; - @tmp = split (/\=/, $date2_in); - 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 $cmd = "$opensslbin pkcs12 -in $filename -out $pem_file"; + system $cmd and die "'$cmd' returned $?"; - my @fields = - $tmp =~ /(\w+)\s*(\d+)\s*(\d+):(\d+):(\d+)\s*(\d+)\s*GMT/; + -e $pem_file and -s $pem_file or die("Conversion of $filename failed."); + handle_add_pem($pem_file); +} - $#fields != 5 and print "Expiration Date: Parse Error : $tmp\n\n" or - timegm($fields[4], $fields[3], $fields[2], $fields[1], - $months{$fields[0]}, $fields[5]) > time and $result = 'e'; - $result eq 'e' and print "Certificate is not yet valid.\n" and return $result; +sub handle_add_chain ($$$) { + my ($key_file, $cert_file, $issuer_file) = @_; - @fields = - $tmp[1] =~ /(\w+)\s*(\d+)\s*(\d+):(\d+):(\d+)\s*(\d+)\s*GMT/; + my $cert_hash = openssl_hash($cert_file); + my $issuer_hash = openssl_hash($issuer_file); - $#fields != 5 and print "Expiration Date: Parse Error : $tmp[1]\n\n" or - timegm($fields[4], $fields[3], $fields[2], $fields[1], - $months{$fields[0]}, $fields[5]) < time and $result = 'e'; - $result eq 'e' and print "Certificate has expired.\n" and return $result; + my $label = query_label(); - } - - if ( defined $crl ) { - my @serial = split (/\=/, $serial_in); - my $cmd = "$opensslbin crl -text -noout -in $crl | grep -A1 $serial[1]"; - (my $l1, my $l2) = `$cmd`; - $? and die "'$cmd' returned $?"; - - if ( defined $l2 ) { - my @revoke_date = split (/:\s/, $l2); - print "FAILURE: Certificate $cert has been revoked on $revoke_date[1]\n"; - $result = 'r'; - } - } - print "\n"; + cm_add_certificate($issuer_file, \$issuer_hash, 0, $label); + my @mailbox = cm_add_certificate($cert_file, \$cert_hash, 1, $label, $issuer_hash); - if ($result eq 'v') { - return 't'; + foreach my $mailbox (@mailbox) { + cm_add_key($key_file, $cert_hash, $mailbox, $label); } +} - return $result; +sub handle_verify_cert ($$) { + my ($keyid, $crl) = @_; + + -e "$certificates_path/$keyid" or $keyid eq 'all' + or die "No such certificate: $keyid"; + cm_modify_entry('V', $keyid, 1, $crl); } +sub handle_remove_pair ($) { + my ($keyid) = @_; + if (-e "$certificates_path/$keyid") { + unlink "$certificates_path/$keyid"; + cm_modify_entry('R', $keyid, 1); + print "Removed certificate $keyid.\n"; + } + else { + die "No such certificate: $keyid"; + } -sub add_root_cert ($) { - my $root_cert = shift or die; + if (-e "$private_keys_path/$keyid") { + unlink "$private_keys_path/$keyid"; + cm_modify_entry('R', $keyid, 0); + print "Removed private key $keyid.\n"; + } +} - my $format = -B $root_cert ? 'DER' : 'PEM'; +sub handle_add_root_cert ($) { + my ($root_cert) = @_; - my $cmd = "$opensslbin x509 -noout -hash -in $root_cert -inform $format"; - my $root_hash = `$cmd`; - $? and die "'$cmd' returned $?"; + my $root_hash = openssl_hash($root_cert); if (-d $root_certs_path) { -e "$root_certs_path/$root_hash" or mycopy $root_cert, "$root_certs_path/$root_hash"; } else { - open(ROOT_CERTS, ">>$root_certs_path") or + open(ROOT_CERTS, ">>$root_certs_path") or die ("Couldn't open $root_certs_path for writing"); - $cmd = "$opensslbin x509 -in $root_cert -inform $format -fingerprint -noout"; - $? and die "'$cmd' returned $?"; - chomp(my $md5fp = `$cmd`); + my $md5fp = openssl_fingerprint($root_cert); + my $format = openssl_format($root_cert); - $cmd = "$opensslbin x509 -in $root_cert -inform $format -text -noout"; + my $cmd = "$opensslbin x509 -in $root_cert -inform $format -text -noout"; $? and die "'$cmd' returned $?"; my @cert_text = `$cmd`; @@ -936,38 +962,5 @@ sub add_root_cert ($) { print ROOT_CERTS @cert_text; close (ROOT_CERTS); } - -} - -sub newfile ($;$$) { - # returns a file name which does not exist for tmp file creation - my $filename = shift; - my $option = shift; - $option = "notemp" if (not defined($option)); - if (! $tmpdir and $option eq "temp") { - $tmpdir = mutt_Q 'tmpdir'; - $tmpdir = newfile("$tmpdir/smime"); - mkdir $tmpdir, 0700 || die "Can't create $tmpdir: $!\n"; - } - $filename = "$tmpdir/$filename" if ($option eq "temp"); - my $newfilename = $filename; - my $count = 0; - while (-e $newfilename) { - $newfilename = "$filename.$count"; - $count++; - } - unshift(@tempfiles,$newfilename); - return $newfilename; -} - - -END { - # remove all our temporary files in the end: - for (@tempfiles){ - if (-f) { - unlink; - } elsif (-d) { - rmdir; - } - } + } -- 2.40.0