mutt stable branch with some hacks
at master 1213 lines 31 kB view raw
1#! /usr/bin/perl -w 2 3# Copyright (C) 2001-2002 Oliver Ehli <elmy@acm.org> 4# Copyright (C) 2001 Mike Schiraldi <raldi@research.netsol.com> 5# Copyright (C) 2003 Bjoern Jacke <bjoern@j3e.de> 6# Copyright (C) 2015 Kevin J. McCarthy <kevin@8t8.us> 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program; if not, write to the Free Software 20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 21 22use strict; 23use File::Copy; 24use File::Glob ':glob'; 25use File::Temp qw(tempfile tempdir); 26 27umask 077; 28 29use Time::Local; 30 31# helper routines 32sub usage (); 33sub mutt_Q ($); 34sub mycopy ($$); 35sub query_label (); 36sub mkdir_recursive ($); 37sub verify_files_exist (@); 38sub create_tempfile (;$); 39sub new_cert_structure (); 40sub create_cert_chains (@); 41 42# openssl helpers 43sub openssl_exec (@); 44sub openssl_format ($); 45sub openssl_x509_query ($@); 46sub openssl_hash ($); 47sub openssl_fingerprint ($); 48sub openssl_emails ($); 49sub openssl_p12_to_pem ($$); 50sub openssl_verify ($$); 51sub openssl_crl_text($); 52sub openssl_trust_flag ($$;$); 53sub openssl_parse_pem ($$); 54sub openssl_dump_cert ($); 55sub openssl_purpose_flag ($); 56 57# key/certificate management methods 58sub cm_list_certs (); 59sub cm_add_entry ($$$$$$;$); 60sub cm_add_cert ($); 61sub cm_add_indexed_cert ($$$); 62sub cm_add_key ($$$$$$); 63sub cm_modify_entry ($$$;$); 64sub cm_find_entry ($$); 65sub cm_refresh_index (); 66 67# op handlers 68sub handle_init_paths (); 69sub handle_change_label ($); 70sub handle_add_cert ($); 71sub handle_add_pem ($); 72sub handle_add_p12 ($); 73sub handle_add_chain ($$$); 74sub handle_verify_cert($$); 75sub handle_remove_pair ($); 76sub handle_add_root_cert ($); 77 78 79my $mutt = $ENV{MUTT_CMDLINE} || 'mutt'; 80my $opensslbin = "/usr/bin/openssl"; 81my $tmpdir; 82 83# Get the directories mutt uses for certificate/key storage. 84 85my $private_keys_path = mutt_Q 'smime_keys'; 86die "smime_keys is not set in mutt's configuration file" 87 if length $private_keys_path == 0; 88 89my $certificates_path = mutt_Q 'smime_certificates'; 90die "smime_certificates is not set in mutt's configuration file" 91 if length $certificates_path == 0; 92 93my $root_certs_path = mutt_Q 'smime_ca_location'; 94die "smime_ca_location is not set in mutt's configuration file" 95 if length $root_certs_path == 0; 96 97my $root_certs_switch; 98if ( -d $root_certs_path) { 99 $root_certs_switch = -CApath; 100} else { 101 $root_certs_switch = -CAfile; 102} 103 104 105###### 106# OPS 107###### 108 109if (@ARGV == 1 and $ARGV[0] eq "init") { 110 handle_init_paths(); 111} 112elsif (@ARGV == 1 and $ARGV[0] eq "refresh") { 113 cm_refresh_index(); 114} 115elsif (@ARGV == 1 and $ARGV[0] eq "list") { 116 cm_list_certs(); 117} 118elsif (@ARGV == 2 and $ARGV[0] eq "label") { 119 handle_change_label($ARGV[1]); 120} 121elsif (@ARGV == 2 and $ARGV[0] eq "add_cert") { 122 verify_files_exist($ARGV[1]); 123 handle_add_cert($ARGV[1]); 124} 125elsif (@ARGV == 2 and $ARGV[0] eq "add_pem") { 126 verify_files_exist($ARGV[1]); 127 handle_add_pem($ARGV[1]); 128} 129elsif ( @ARGV == 2 and $ARGV[0] eq "add_p12") { 130 verify_files_exist($ARGV[1]); 131 handle_add_p12($ARGV[1]); 132} 133elsif (@ARGV == 4 and $ARGV[0] eq "add_chain") { 134 verify_files_exist($ARGV[1], $ARGV[2], $ARGV[3]); 135 handle_add_chain($ARGV[1], $ARGV[2], $ARGV[3]); 136} 137elsif ((@ARGV == 2 or @ARGV == 3) and $ARGV[0] eq "verify") { 138 verify_files_exist($ARGV[2]) if (@ARGV == 3); 139 handle_verify_cert($ARGV[1], $ARGV[2]); 140} 141elsif (@ARGV == 2 and $ARGV[0] eq "remove") { 142 handle_remove_pair($ARGV[1]); 143} 144elsif (@ARGV == 2 and $ARGV[0] eq "add_root") { 145 verify_files_exist($ARGV[1]); 146 handle_add_root_cert($ARGV[1]); 147} 148else { 149 usage(); 150 exit(1); 151} 152 153exit(0); 154 155 156############## sub-routines ######################## 157 158 159################### 160# helper routines 161################### 162 163sub usage () { 164 print <<EOF; 165 166Usage: smime_keys <operation> [file(s) | keyID [file(s)]] 167 168 with operation being one of: 169 170 init : no files needed, inits directory structure. 171 refresh : refreshes certificate and key index files. 172 Updates trust flag (expiration). 173 Adds purpose flag if missing. 174 175 list : lists the certificates stored in database. 176 label : keyID required. changes/removes/adds label. 177 remove : keyID required. 178 verify : 1=keyID and optionally 2=CRL 179 Verifies the certificate chain, and optionally wether 180 this certificate is included in supplied CRL (PEM format). 181 Note: to verify all certificates at the same time, 182 replace keyID with "all" 183 184 add_cert : certificate required. 185 add_chain : three files reqd: 1=Key, 2=certificate 186 plus 3=intermediate certificate(s). 187 add_p12 : one file reqd. Adds keypair to database. 188 file is PKCS12 (e.g. export from netscape). 189 add_pem : one file reqd. Adds keypair to database. 190 (file was converted from e.g. PKCS12). 191 192 add_root : one file reqd. Adds PEM root certificate to the location 193 specified within muttrc (smime_verify_* command) 194 195EOF 196} 197 198sub mutt_Q ($) { 199 my ($var) = @_; 200 201 my $cmd = "$mutt -v >/dev/null 2>/dev/null"; 202 system ($cmd) == 0 or die<<EOF; 203Couldn't launch mutt. I attempted to do so by running the command "$mutt". 204If that's not the right command, you can override it by setting the 205environment variable \$MUTT_CMDLINE 206EOF 207 208 $cmd = "$mutt -Q $var 2>/dev/null"; 209 my $answer = `$cmd`; 210 211 $? and die<<EOF; 212Couldn't look up the value of the mutt variable "$var". 213You must set this in your mutt config file. See contrib/smime.rc for an example. 214EOF 215 216 $answer =~ /\"(.*?)\"/ and return bsd_glob($1, GLOB_TILDE | GLOB_NOCHECK); 217 218 $answer =~ /^Mutt (.*?) / and die<<EOF; 219This script requires mutt 1.5.0 or later. You are using mutt $1. 220EOF 221 222 die "Value of $var is weird\n"; 223} 224 225sub mycopy ($$) { 226 my ($source, $dest) = @_; 227 228 copy $source, $dest or die "Problem copying $source to $dest: $!\n"; 229} 230 231sub query_label () { 232 my $input; 233 my $label; 234 my $junk; 235 236 print "\nYou may assign a label to this key, so you don't have to remember\n"; 237 print "the key ID. This has to be _one_ word (no whitespaces).\n\n"; 238 239 print "Enter label: "; 240 $input = <STDIN>; 241 242 if (defined($input) && ($input !~ /^\s*$/)) { 243 chomp($input); 244 $input =~ s/^\s+//; 245 ($label, $junk) = split(/\s/, $input, 2); 246 247 if (defined($junk)) { 248 print "\nUsing '$label' as label; ignoring '$junk'\n"; 249 } 250 } 251 252 if ((! defined($label)) || ($label =~ /^\s*$/)) { 253 $label = "-"; 254 } 255 256 return $label; 257} 258 259sub mkdir_recursive ($) { 260 my ($path) = @_; 261 my $tmp_path; 262 263 for my $dir (split /\//, $path) { 264 $tmp_path .= "$dir/"; 265 266 -d $tmp_path 267 or mkdir $tmp_path, 0700 268 or die "Can't mkdir $tmp_path: $!"; 269 } 270} 271 272sub verify_files_exist (@) { 273 my (@files) = @_; 274 275 foreach my $file (@files) { 276 if ((! -e $file) || (! -s $file)) { 277 die("$file is nonexistent or empty."); 278 } 279 } 280} 281 282# Returns a list ($fh, $filename) 283sub create_tempfile (;$) { 284 my ($directory) = @_; 285 286 if (! defined($directory)) { 287 if (! defined($tmpdir)) { 288 $tmpdir = tempdir(CLEANUP => 1); 289 } 290 $directory = $tmpdir; 291 } 292 293 return tempfile(DIR => $directory); 294} 295 296# Creates a cert data structure used by openssl_parse_pem 297sub new_cert_structure () { 298 my $cert_data = {}; 299 300 $cert_data->{datafile} = ""; 301 $cert_data->{type} = ""; 302 $cert_data->{localKeyID} = ""; 303 $cert_data->{subject} = ""; 304 $cert_data->{issuer} = ""; 305 306 return $cert_data; 307} 308 309sub create_cert_chains (@) { 310 my (@certs) = @_; 311 312 my (%subject_hash, @leaves, @chains); 313 314 foreach my $cert (@certs) { 315 $cert->{children} = 0; 316 if ($cert->{subject}) { 317 $subject_hash{$cert->{subject}} = $cert; 318 } 319 } 320 321 foreach my $cert (@certs) { 322 my $parent = $subject_hash{$cert->{issuer}}; 323 if (defined($parent)) { 324 $parent->{children} += 1; 325 } 326 } 327 328 @leaves = grep { $_->{children} == 0 } @certs; 329 foreach my $leaf (@leaves) { 330 my $chain = []; 331 my $cert = $leaf; 332 333 while (defined($cert)) { 334 push @$chain, $cert; 335 336 $cert = $subject_hash{$cert->{issuer}}; 337 if (defined($cert) && 338 (scalar(grep {$_ == $cert} @$chain) != 0)) { 339 $cert = undef; 340 } 341 } 342 343 push @chains, $chain; 344 } 345 346 return @chains; 347} 348 349 350################## 351# openssl helpers 352################## 353 354sub openssl_exec (@) { 355 my (@args) = @_; 356 357 my $fh; 358 359 open($fh, "-|", $opensslbin, @args) 360 or die "Failed to run '$opensslbin @args': $!"; 361 my @output = <$fh>; 362 if (! close($fh)) { 363 # NOTE: Callers should check the value of $? for the exit status. 364 if ($!) { 365 die "Syserr closing '$opensslbin @args' pipe: $!"; 366 } 367 } 368 369 return @output; 370} 371 372sub openssl_format ($) { 373 my ($filename) = @_; 374 375 return -B $filename ? 'DER' : 'PEM'; 376} 377 378sub openssl_x509_query ($@) { 379 my ($filename, @query) = @_; 380 381 my $format = openssl_format($filename); 382 my @args = ("x509", "-in", $filename, "-inform", $format, "-noout", @query); 383 return openssl_exec(@args); 384} 385 386sub openssl_hash ($) { 387 my ($filename) = @_; 388 389 my $cert_hash = join("", openssl_x509_query($filename, "-hash")); 390 $? and die "openssl -hash '$filename' returned $?"; 391 392 chomp($cert_hash); 393 return $cert_hash; 394} 395 396sub openssl_fingerprint ($) { 397 my ($filename) = @_; 398 399 my $fingerprint = join("", openssl_x509_query($filename, "-fingerprint")); 400 $? and die "openssl -fingerprint '$filename' returned $?"; 401 402 chomp($fingerprint); 403 return $fingerprint; 404} 405 406sub openssl_emails ($) { 407 my ($filename) = @_; 408 409 my @mailboxes = openssl_x509_query($filename, "-email"); 410 $? and die "openssl -email '$filename' returned $?"; 411 412 chomp(@mailboxes); 413 return @mailboxes; 414} 415 416sub openssl_p12_to_pem ($$) { 417 my ($p12_file, $pem_file) = @_; 418 419 my @args = ("pkcs12", "-in", $p12_file, "-out", $pem_file); 420 openssl_exec(@args); 421 $? and die "openssl pkcs12 conversion returned $?"; 422} 423 424sub openssl_verify ($$) { 425 my ($issuer_path, $cert_path) = @_; 426 427 my @args = ("verify", $root_certs_switch, $root_certs_path, 428 "-purpose", "smimesign", "-purpose", "smimeencrypt", "-untrusted", 429 $issuer_path, $cert_path); 430 my $output = join("", openssl_exec(@args)); 431 432 chomp($output); 433 return $output; 434} 435 436sub openssl_crl_text($) { 437 my ($crl) = @_; 438 439 my @args = ("crl", "-text", "-noout", "-in", $crl); 440 my @output = openssl_exec(@args); 441 $? and die "openssl crl -text '$crl' returned $?"; 442 443 return @output; 444} 445 446sub openssl_trust_flag ($$;$) { 447 my ($cert, $issuerid, $crl) = @_; 448 449 print "==> about to verify certificate of $cert\n"; 450 451 my $result = 't'; 452 my $issuer_path; 453 my $cert_path = "$certificates_path/$cert"; 454 455 if ($issuerid eq '?') { 456 $issuer_path = "$certificates_path/$cert"; 457 } else { 458 $issuer_path = "$certificates_path/$issuerid"; 459 } 460 461 my $output = openssl_verify($issuer_path, $cert_path); 462 if ($?) { 463 print "openssl verify returned exit code " . ($? >> 8) . " with output:\n"; 464 print "$output\n\n"; 465 print "Marking certificate as invalid\n"; 466 return 'i'; 467 } 468 print "\n$output\n"; 469 470 if ($output !~ /OK/) { 471 return 'i'; 472 } 473 474 my ($not_before, $not_after, $serial_in) = openssl_x509_query($cert_path, "-dates", "-serial"); 475 $? and die "openssl -dates -serial '$cert_path' returned $?"; 476 477 if ( defined $not_before and defined $not_after ) { 478 my %months = ('Jan', '00', 'Feb', '01', 'Mar', '02', 'Apr', '03', 479 'May', '04', 'Jun', '05', 'Jul', '06', 'Aug', '07', 480 'Sep', '08', 'Oct', '09', 'Nov', '10', 'Dec', '11'); 481 482 my @tmp = split (/\=/, $not_before); 483 my $not_before_date = $tmp[1]; 484 my @fields = 485 $not_before_date =~ /(\w+)\s*(\d+)\s*(\d+):(\d+):(\d+)\s*(\d+)\s*GMT/; 486 if ($#fields == 5) { 487 if (timegm($fields[4], $fields[3], $fields[2], $fields[1], 488 $months{$fields[0]}, $fields[5]) > time) { 489 print "Certificate is not yet valid.\n"; 490 return 'e'; 491 } 492 } else { 493 print "Expiration Date: Parse Error : $not_before_date\n\n"; 494 } 495 496 @tmp = split (/\=/, $not_after); 497 my $not_after_date = $tmp[1]; 498 @fields = 499 $not_after_date =~ /(\w+)\s*(\d+)\s*(\d+):(\d+):(\d+)\s*(\d+)\s*GMT/; 500 if ($#fields == 5) { 501 if (timegm($fields[4], $fields[3], $fields[2], $fields[1], 502 $months{$fields[0]}, $fields[5]) < time) { 503 print "Certificate has expired.\n"; 504 return 'e'; 505 } 506 } else { 507 print "Expiration Date: Parse Error : $not_after_date\n\n"; 508 } 509 } 510 511 if ( defined $crl ) { 512 chomp($serial_in); 513 my @serial = split (/\=/, $serial_in); 514 my $match_line = undef; 515 my @crl_lines = openssl_crl_text($crl); 516 for (my $index = 0; $index <= $#crl_lines; $index++) { 517 if ($crl_lines[$index] =~ /Serial Number:\s*\Q$serial[1]\E\b/) { 518 $match_line = $crl_lines[$index + 1]; 519 last; 520 } 521 } 522 523 if ( defined $match_line ) { 524 my @revoke_date = split (/:\s/, $match_line); 525 print "FAILURE: Certificate $cert has been revoked on $revoke_date[1]\n"; 526 $result = 'r'; 527 } 528 } 529 print "\n"; 530 531 return $result; 532} 533 534sub openssl_parse_pem ($$) { 535 my ($filename, $attrs_required) = @_; 536 537 my $state = 0; 538 my $cert_data; 539 my @certs; 540 my $cert_count = 0; 541 my $bag_count = 0; 542 my $cert_tmp_fh; 543 my $cert_tmp_filename; 544 545 $cert_data = new_cert_structure(); 546 ($cert_tmp_fh, $cert_data->{datafile}) = create_tempfile(); 547 548 open(PEM_FILE, "<$filename") or die("Can't open $filename: $!"); 549 while (<PEM_FILE>) { 550 if (/^Bag Attributes/) { 551 $bag_count++; 552 $state == 0 or die("PEM-parse error at: $."); 553 $state = 1; 554 } 555 556 # Allow attributes without the "Bag Attributes" header 557 if ($state != 2) { 558 if (/localKeyID:\s*(.*)/) { 559 $cert_data->{localKeyID} = $1; 560 } 561 562 if (/subject=\s*(.*)/) { 563 $cert_data->{subject} = $1; 564 } 565 566 if (/issuer=\s*(.*)/) { 567 $cert_data->{issuer} = $1; 568 } 569 } 570 571 572 if (/^-----/) { 573 if (/BEGIN/) { 574 print $cert_tmp_fh $_; 575 $state = 2; 576 577 if (/PRIVATE/) { 578 $cert_data->{type} = "K"; 579 next; 580 } 581 if (/CERTIFICATE/) { 582 $cert_data->{type} = "C"; 583 next; 584 } 585 die("What's this: $_"); 586 } 587 if (/END/) { 588 $state = 0; 589 print $cert_tmp_fh $_; 590 close($cert_tmp_fh); 591 592 $cert_count++; 593 push (@certs, $cert_data); 594 595 $cert_data = new_cert_structure(); 596 ($cert_tmp_fh, $cert_data->{datafile}) = create_tempfile(); 597 next; 598 } 599 } 600 print $cert_tmp_fh $_; 601 } 602 close($cert_tmp_fh); 603 close(PEM_FILE); 604 605 if ($attrs_required && ($bag_count != $cert_count)) { 606 die("Not all contents were bagged. can't continue."); 607 } 608 609 return @certs; 610} 611 612sub openssl_dump_cert ($) { 613 my ($filename) = @_; 614 615 my $format = openssl_format($filename); 616 my @args = ("x509", "-in", $filename, "-inform", $format); 617 my $output = join("", openssl_exec(@args)); 618 $? and die "openssl x509 certicate dump returned $?"; 619 620 return $output; 621} 622 623sub openssl_purpose_flag ($) { 624 my ($filename) = @_; 625 626 my $purpose = ""; 627 628 my @output = openssl_x509_query($filename, "-purpose"); 629 $? and die "openssl -purpose '$filename' returned $?"; 630 631 foreach my $line (@output) { 632 if ($line =~ /^S\/MIME signing\s*:\s*Yes/) { 633 $purpose .= "s"; 634 } 635 elsif ($line =~ /^S\/MIME encryption\s*:\s*Yes/) { 636 $purpose .= "e"; 637 } 638 } 639 640 if (! $purpose) { 641 $purpose = "-"; 642 } 643 644 return $purpose; 645} 646 647 648################################# 649# certificate management methods 650################################# 651 652sub cm_list_certs () { 653 my %keyflags = ( 'i', '(Invalid)', 'r', '(Revoked)', 'e', '(Expired)', 654 'u', '(Unverified)', 'v', '(Valid)', 't', '(Trusted)'); 655 656 open(INDEX, "<$certificates_path/.index") or 657 die "Couldn't open $certificates_path/.index: $!"; 658 659 print "\n"; 660 while (<INDEX>) { 661 my $tmp; 662 my @tmp; 663 my $tab = " "; 664 my @fields = split; 665 666 if ($fields[2] eq '-') { 667 print "$fields[1]: Issued for: $fields[0] $keyflags{$fields[4]}\n"; 668 } else { 669 print "$fields[1]: Issued for: $fields[0] \"$fields[2]\" $keyflags{$fields[4]}\n"; 670 } 671 672 my $certfile = "$certificates_path/$fields[1]"; 673 my $cert; 674 { 675 open F, $certfile or 676 die "Couldn't open $certfile: $!"; 677 local $/; 678 $cert = <F>; 679 close F; 680 } 681 682 my ($subject_in, $issuer_in, $date1_in, $date2_in) = 683 openssl_x509_query($certfile, "-subject", "-issuer", "-dates"); 684 $? and print "ERROR: openssl -subject -issuer -dates '$certfile' returned $?\n\n" and next; 685 686 687 my @subject = split(/\//, $subject_in); 688 while (@subject) { 689 $tmp = shift @subject; 690 ($tmp =~ /^CN\=/) and last; 691 undef $tmp; 692 } 693 defined $tmp and @tmp = split (/\=/, $tmp) and 694 print $tab."Subject: $tmp[1]\n"; 695 696 my @issuer = split(/\//, $issuer_in); 697 while (@issuer) { 698 $tmp = shift @issuer; 699 ($tmp =~ /^CN\=/) and last; 700 undef $tmp; 701 } 702 defined $tmp and @tmp = split (/\=/, $tmp) and 703 print $tab."Issued by: $tmp[1]"; 704 705 if ( defined $date1_in and defined $date2_in ) { 706 @tmp = split (/\=/, $date1_in); 707 $tmp = $tmp[1]; 708 @tmp = split (/\=/, $date2_in); 709 print $tab."Certificate is not valid before $tmp". 710 $tab." or after ".$tmp[1]; 711 } 712 713 -e "$private_keys_path/$fields[1]" and 714 print "$tab - Matching private key installed -\n"; 715 716 my @purpose = openssl_x509_query($certfile, "-purpose"); 717 $? and die "openssl -purpose '$certfile' returned $?"; 718 chomp(@purpose); 719 720 print "$tab$purpose[0] (displays S/MIME options only)\n"; 721 while (@purpose) { 722 $tmp = shift @purpose; 723 ($tmp =~ /^S\/MIME/ and $tmp =~ /Yes/) or next; 724 my @tmptmp = split (/:/, $tmp); 725 print "$tab $tmptmp[0]\n"; 726 } 727 728 print "\n"; 729 } 730 731 close(INDEX); 732} 733 734sub cm_add_entry ($$$$$$;$) { 735 my ($mailbox, $hashvalue, $use_cert, $label, $trust, $purpose, $issuer_hash) = @_; 736 737 if (! defined($issuer_hash) ) { 738 $issuer_hash = "?"; 739 } 740 741 if ($use_cert) { 742 open(INDEX, "+<$certificates_path/.index") or 743 die "Couldn't open $certificates_path/.index: $!"; 744 } 745 else { 746 open(INDEX, "+<$private_keys_path/.index") or 747 die "Couldn't open $private_keys_path/.index: $!"; 748 } 749 750 while (<INDEX>) { 751 my @fields = split; 752 if (($fields[0] eq $mailbox) && ($fields[1] eq $hashvalue)) { 753 close(INDEX); 754 return; 755 } 756 } 757 758 print INDEX "$mailbox $hashvalue $label $issuer_hash $trust $purpose\n"; 759 760 close(INDEX); 761} 762 763# Returns the hashvalue.index of the stored cert 764sub cm_add_cert ($) { 765 my ($filename) = @_; 766 767 my $iter = 0; 768 my $hashvalue = openssl_hash($filename); 769 my $fp1 = openssl_fingerprint($filename); 770 771 while (-e "$certificates_path/$hashvalue.$iter") { 772 my $fp2 = openssl_fingerprint("$certificates_path/$hashvalue.$iter"); 773 774 last if $fp1 eq $fp2; 775 $iter++; 776 } 777 $hashvalue .= ".$iter"; 778 779 if (-e "$certificates_path/$hashvalue") { 780 print "\nCertificate: $certificates_path/$hashvalue already installed.\n"; 781 } 782 else { 783 mycopy $filename, "$certificates_path/$hashvalue"; 784 } 785 786 return $hashvalue; 787} 788 789# Returns a reference containing the hashvalue, mailboxes, trust flag, and purpose 790# flag of the stored cert. 791sub cm_add_indexed_cert ($$$) { 792 my ($filename, $label, $issuer_hash) = @_; 793 794 my $cert_data = {}; 795 796 $cert_data->{hashvalue} = cm_add_cert($filename); 797 $cert_data->{mailboxes} = [ openssl_emails($filename) ]; 798 $cert_data->{trust} = openssl_trust_flag($cert_data->{hashvalue}, $issuer_hash); 799 $cert_data->{purpose} = openssl_purpose_flag($filename); 800 801 foreach my $mailbox (@{$cert_data->{mailboxes}}) { 802 cm_add_entry($mailbox, $cert_data->{hashvalue}, 1, $label, 803 $cert_data->{trust}, $cert_data->{purpose}, $issuer_hash); 804 print "\ncertificate ", $cert_data->{hashvalue}, " ($label) for $mailbox added.\n"; 805 } 806 807 return $cert_data; 808} 809 810sub cm_add_key ($$$$$$) { 811 my ($file, $hashvalue, $mailbox, $label, $trust, $purpose) = @_; 812 813 unless (-e "$private_keys_path/$hashvalue") { 814 mycopy $file, "$private_keys_path/$hashvalue"; 815 } 816 817 cm_add_entry($mailbox, $hashvalue, 0, $label, $trust, $purpose); 818 print "added private key: " . 819 "$private_keys_path/$hashvalue for $mailbox\n"; 820} 821 822sub cm_modify_entry ($$$;$) { 823 my ($op, $hashvalue, $use_cert, $opt_param) = @_; 824 825 my $label; 826 my $trust; 827 my $purpose; 828 my $path; 829 my @fields; 830 831 $op eq 'L' and ($label = $opt_param); 832 $op eq 'T' and ($trust = $opt_param); 833 $op eq 'P' and ($purpose = $opt_param); 834 835 if ($use_cert) { 836 $path = $certificates_path; 837 } 838 else { 839 $path = $private_keys_path; 840 } 841 842 open(INDEX, "<$path/.index") or 843 die "Couldn't open $path/.index: $!"; 844 my ($newindex_fh, $newindex) = create_tempfile(); 845 846 while (<INDEX>) { 847 chomp; 848 849 # fields: mailbox hash label issuer_hash trust purpose 850 @fields = split; 851 852 if ($fields[1] eq $hashvalue or $hashvalue eq 'all') { 853 $op eq 'R' and next; 854 855 if ($op eq 'L') { 856 $fields[2] = $label; 857 } 858 859 if ($op eq 'T') { 860 $fields[3] = "?" if ($#fields < 3); 861 $fields[4] = $trust; 862 } 863 864 if ($op eq 'P') { 865 $fields[3] = "?" if ($#fields < 3); 866 $fields[4] = "u" if ($#fields < 4); 867 $fields[5] = $purpose; 868 } 869 870 print $newindex_fh join(" ", @fields), "\n"; 871 } 872 else { 873 print $newindex_fh $_, "\n"; 874 } 875 } 876 close(INDEX); 877 close($newindex_fh); 878 879 move $newindex, "$path/.index" 880 or die "Couldn't move $newindex to $path/.index: $!\n"; 881} 882 883# This returns the first matching entry. 884sub cm_find_entry ($$) { 885 my ($hashvalue, $use_cert) = @_; 886 887 my ($path, $index_fh); 888 889 if ($use_cert) { 890 $path = $certificates_path; 891 } 892 else { 893 $path = $private_keys_path; 894 } 895 896 open($index_fh, "<$path/.index") or 897 die "Couldn't open $path/.index: $!"; 898 899 while (<$index_fh>) { 900 chomp; 901 my @fields = split; 902 if ($fields[1] eq $hashvalue) { 903 close($index_fh); 904 return @fields; 905 } 906 } 907 908 close($index_fh); 909 return; 910} 911 912# Refreshes trust flags, and adds purpose if missing 913# (e.g. from an older index format) 914sub cm_refresh_index () { 915 my $index_fh; 916 917 my ($last_hash, $last_trust, $last_purpose) = ("", "", ""); 918 919 open($index_fh, "<$certificates_path/.index") or 920 die "Couldn't open $certificates_path/.index: $!"; 921 my ($newindex_fh, $newindex) = create_tempfile(); 922 923 while (<$index_fh>) { 924 chomp; 925 926 # fields: mailbox hash label issuer_hash trust purpose 927 my @fields = split; 928 929 if ($fields[1] eq $last_hash) { 930 $fields[4] = $last_trust; 931 $fields[5] = $last_purpose; 932 } 933 else { 934 # Don't overwrite a revoked flag, because we don't have the CRL 935 if ($fields[4] ne "r") { 936 $fields[4] = openssl_trust_flag($fields[1], $fields[3]); 937 } 938 939 if ($#fields < 5) { 940 $fields[5] = openssl_purpose_flag("$certificates_path/$fields[1]"); 941 } 942 943 # To update an old private keys index format, always push the trust 944 # and purpose out. 945 if (-e "$private_keys_path/$fields[1]") { 946 cm_modify_entry ("T", $fields[1], 0, $fields[4]); 947 cm_modify_entry ("P", $fields[1], 0, $fields[5]); 948 } 949 950 $last_hash = $fields[1]; 951 $last_trust = $fields[4]; 952 $last_purpose = $fields[5]; 953 } 954 955 print $newindex_fh join(" ", @fields), "\n"; 956 } 957 close($index_fh); 958 close($newindex_fh); 959 960 move $newindex, "$certificates_path/.index" 961 or die "Couldn't move $newindex to $certificates_path/.index: $!\n"; 962} 963 964 965############## 966# Op handlers 967############## 968 969sub handle_init_paths () { 970 mkdir_recursive($certificates_path); 971 mkdir_recursive($private_keys_path); 972 973 my $file; 974 975 $file = $certificates_path . "/.index"; 976 -f $file or open(TMP_FILE, ">$file") and close(TMP_FILE) 977 or die "Can't touch $file: $!"; 978 979 $file = $private_keys_path . "/.index"; 980 -f $file or open(TMP_FILE, ">$file") and close(TMP_FILE) 981 or die "Can't touch $file: $!"; 982} 983 984sub handle_change_label ($) { 985 my ($keyid) = @_; 986 987 my $label = query_label(); 988 989 if (-e "$certificates_path/$keyid") { 990 cm_modify_entry('L', $keyid, 1, $label); 991 print "Changed label for certificate $keyid.\n"; 992 } 993 else { 994 die "No such certificate: $keyid"; 995 } 996 997 if (-e "$private_keys_path/$keyid") { 998 cm_modify_entry('L', $keyid, 0, $label); 999 print "Changed label for private key $keyid.\n"; 1000 } 1001} 1002 1003sub handle_add_cert($) { 1004 my ($filename) = @_; 1005 1006 my $label = query_label(); 1007 my @cert_contents = openssl_parse_pem($filename, 0); 1008 @cert_contents = grep { $_->{type} eq "C" } @cert_contents; 1009 1010 my @cert_chains = create_cert_chains(@cert_contents); 1011 print "Found " . scalar(@cert_chains) . " certificate chains\n"; 1012 1013 foreach my $chain (@cert_chains) { 1014 my $leaf = shift(@$chain); 1015 my $issuer_chain_hash = "?"; 1016 1017 print "Processing chain:\n"; 1018 if ($leaf->{subject}) { 1019 print "subject=" . $leaf->{subject} . "\n"; 1020 } 1021 1022 if (scalar(@$chain) > 0) { 1023 my ($issuer_chain_fh, $issuer_chain_file) = create_tempfile(); 1024 1025 foreach my $issuer (@$chain) { 1026 my $issuer_datafile = $issuer->{datafile}; 1027 open(my $issuer_fh, "< $issuer_datafile") or 1028 die "can't open $issuer_datafile: $?"; 1029 print $issuer_chain_fh $_ while (<$issuer_fh>); 1030 close($issuer_fh); 1031 } 1032 1033 close($issuer_chain_fh); 1034 $issuer_chain_hash = cm_add_cert($issuer_chain_file); 1035 } 1036 1037 cm_add_indexed_cert($leaf->{datafile}, $label, $issuer_chain_hash); 1038 } 1039} 1040 1041sub handle_add_pem ($) { 1042 my ($filename) = @_; 1043 1044 my @pem_contents; 1045 my $iter; 1046 my $key; 1047 my $certificate; 1048 my $root_cert; 1049 my $issuer_cert_file; 1050 1051 @pem_contents = openssl_parse_pem($filename, 1); 1052 1053 # look for key 1054 $iter = 0; 1055 while ($iter <= $#pem_contents) { 1056 if ($pem_contents[$iter]->{type} eq "K") { 1057 $key = $pem_contents[$iter]; 1058 splice(@pem_contents, $iter, 1); 1059 last; 1060 } 1061 $iter++; 1062 } 1063 defined($key) or die("Couldn't find private key!"); 1064 $key->{localKeyID} or die("Attribute 'localKeyID' wasn't set."); 1065 1066 # private key and certificate use the same 'localKeyID' 1067 $iter = 0; 1068 while ($iter <= $#pem_contents) { 1069 if (($pem_contents[$iter]->{type} eq "C") && 1070 ($pem_contents[$iter]->{localKeyID} eq $key->{localKeyID})) { 1071 $certificate = $pem_contents[$iter]; 1072 splice(@pem_contents, $iter, 1); 1073 last; 1074 } 1075 $iter++; 1076 } 1077 defined($certificate) or die("Couldn't find matching certificate!"); 1078 1079 if ($#pem_contents < 0) { 1080 die("No root and no intermediate certificates. Can't continue."); 1081 } 1082 1083 # Look for a self signed root certificate 1084 $iter = 0; 1085 while ($iter <= $#pem_contents) { 1086 if ($pem_contents[$iter]->{subject} eq $pem_contents[$iter]->{issuer}) { 1087 $root_cert = $pem_contents[$iter]; 1088 splice(@pem_contents, $iter, 1); 1089 last; 1090 } 1091 $iter++; 1092 } 1093 if (defined($root_cert)) { 1094 $issuer_cert_file = $root_cert->{datafile}; 1095 } else { 1096 print "Couldn't identify root certificate!\n"; 1097 } 1098 1099 # what's left are intermediate certificates. 1100 if ($#pem_contents >= 0) { 1101 my ($tmp_issuer_cert_fh, $tmp_issuer_cert) = create_tempfile(); 1102 $issuer_cert_file = $tmp_issuer_cert; 1103 1104 $iter = 0; 1105 while ($iter <= $#pem_contents) { 1106 my $cert_datafile = $pem_contents[$iter]->{datafile}; 1107 open (CERT, "< $cert_datafile") or die "can't open $cert_datafile: $?"; 1108 print $tmp_issuer_cert_fh $_ while (<CERT>); 1109 close CERT; 1110 1111 $iter++; 1112 } 1113 close $tmp_issuer_cert_fh; 1114 } 1115 1116 handle_add_chain($key->{datafile}, $certificate->{datafile}, $issuer_cert_file); 1117} 1118 1119sub handle_add_p12 ($) { 1120 my ($filename) = @_; 1121 1122 print "\nNOTE: This will ask you for two passphrases:\n"; 1123 print " 1. The passphrase you used for exporting\n"; 1124 print " 2. The passphrase you wish to secure your private key with.\n\n"; 1125 1126 my ($pem_fh, $pem_file) = create_tempfile(); 1127 close($pem_fh); 1128 1129 openssl_p12_to_pem($filename, $pem_file); 1130 -e $pem_file and -s $pem_file or die("Conversion of $filename failed."); 1131 1132 handle_add_pem($pem_file); 1133} 1134 1135sub handle_add_chain ($$$) { 1136 my ($key_file, $cert_file, $issuer_file) = @_; 1137 1138 my $label = query_label(); 1139 1140 my $issuer_hash = cm_add_cert($issuer_file); 1141 my $cert_data = cm_add_indexed_cert($cert_file, $label, $issuer_hash); 1142 1143 foreach my $mailbox (@{$cert_data->{mailboxes}}) { 1144 cm_add_key($key_file, $cert_data->{hashvalue}, $mailbox, $label, 1145 $cert_data->{trust}, $cert_data->{purpose}); 1146 } 1147} 1148 1149sub handle_verify_cert ($$) { 1150 my ($keyid, $crl) = @_; 1151 1152 -e "$certificates_path/$keyid" or $keyid eq 'all' 1153 or die "No such certificate: $keyid"; 1154 1155 my @fields = cm_find_entry($keyid, 1); 1156 if (scalar(@fields)) { 1157 my $issuer_hash = $fields[3]; 1158 my $trust = openssl_trust_flag($keyid, $issuer_hash, $crl); 1159 1160 cm_modify_entry('T', $keyid, 0, $trust); 1161 cm_modify_entry('T', $keyid, 1, $trust); 1162 } 1163} 1164 1165sub handle_remove_pair ($) { 1166 my ($keyid) = @_; 1167 1168 if (-e "$certificates_path/$keyid") { 1169 unlink "$certificates_path/$keyid"; 1170 cm_modify_entry('R', $keyid, 1); 1171 print "Removed certificate $keyid.\n"; 1172 } 1173 else { 1174 die "No such certificate: $keyid"; 1175 } 1176 1177 if (-e "$private_keys_path/$keyid") { 1178 unlink "$private_keys_path/$keyid"; 1179 cm_modify_entry('R', $keyid, 0); 1180 print "Removed private key $keyid.\n"; 1181 } 1182} 1183 1184sub handle_add_root_cert ($) { 1185 my ($root_cert) = @_; 1186 1187 my $root_hash = openssl_hash($root_cert); 1188 1189 if (-d $root_certs_path) { 1190 -e "$root_certs_path/$root_hash" or 1191 mycopy $root_cert, "$root_certs_path/$root_hash"; 1192 } 1193 else { 1194 open(ROOT_CERTS, ">>$root_certs_path") or 1195 die ("Couldn't open $root_certs_path for writing"); 1196 1197 my $md5fp = openssl_fingerprint($root_cert); 1198 1199 my @cert_text = openssl_x509_query($root_cert, "-text"); 1200 $? and die "openssl -text '$root_cert' returned $?"; 1201 1202 print "Enter a label, name or description for this certificate: "; 1203 my $input = <STDIN>; 1204 1205 my $line = "=======================================\n"; 1206 print ROOT_CERTS "\n$input$line$md5fp\nPEM-Data:\n"; 1207 1208 my $cert = openssl_dump_cert($root_cert); 1209 print ROOT_CERTS $cert; 1210 print ROOT_CERTS @cert_text; 1211 close (ROOT_CERTS); 1212 } 1213}