strip spaces from fingerprints
[pgp-tools.git] / caff / caff
index dd74de713e7d963d779e02de22bdaf3ea679bef3..a0a0ed64c270b2268b1430b044b39cab8e3c2a31 100755 (executable)
--- a/caff/caff
+++ b/caff/caff
@@ -85,6 +85,10 @@ Do not sign the keys.
 
 Select the key that is used for signing, in case you have more than one key.
 
 
 Select the key that is used for signing, in case you have more than one key.
 
+=item B<--key-file> I<file>
+
+Import keys from file. Can be supplied more than once.
+
 =back
 
 =head1 FILES
 =back
 
 =head1 FILES
@@ -93,11 +97,20 @@ Select the key that is used for signing, in case you have more than one key.
 
 =item $HOME/.caffrc  -  configuration file
 
 
 =item $HOME/.caffrc  -  configuration file
 
+=item $HOME/.caff/keys/yyyy-mm-dd/  -  processed keys
+
+=item $HOME/.caff/gnupghome/  -  caff's working dir for gpg
+
+=item $HOME/.caff/gnupghome/gpg.conf  -  gpg configuration
+
+useful options include use-agent, default-cert-level, etc.
+
 =back
 
 =head1 CONFIGURATION FILE OPTIONS
 
 The configuration file is a perl script that sets values in the hash B<%CONFIG>.
 =back
 
 =head1 CONFIGURATION FILE OPTIONS
 
 The configuration file is a perl script that sets values in the hash B<%CONFIG>.
+The file is generated when it does not exist.
 
 Example:
 
 
 Example:
 
@@ -169,6 +182,10 @@ Keyserver to download keys from.  Default: B<subkeys.pgp.net>.
 If true, then skip the step of fetching keys from the keyserver.
 Default: B<0>.
 
 If true, then skip the step of fetching keys from the keyserver.
 Default: B<0>.
 
+=item B<key-files> [list of files]
+
+A list of files containing keys to be imported.
+
 =head2 Signing settings
 
 =item B<no-sign> [boolean]
 =head2 Signing settings
 
 =item B<no-sign> [boolean]
@@ -218,6 +235,10 @@ The UIDs for which signatures are included in the mail.
 
 =back
 
 
 =back
 
+=item B<reply-to> [string]
+
+Add a Reply-To: header to messages sent. Default: none.
+
 =item B<bcc> [string]
 
 Address to send blind carbon copies to when sending mail.
 =item B<bcc> [string]
 
 Address to send blind carbon copies to when sending mail.
@@ -239,6 +260,10 @@ Default: none.
 
 http://pgp-tools.alioth.debian.org/
 
 
 http://pgp-tools.alioth.debian.org/
 
+=head1 SEE ALSO
+
+gpg(1), pgp-clean(1), /usr/share/doc/signing-party/examples/caffrc.sample.
+
 =cut
 
 use strict;
 =cut
 
 use strict;
@@ -258,20 +283,118 @@ my $REVISION = '$Rev$';
 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
 my $VERSION = "0.0.0.$REVISION_NUMER";
 
 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
 my $VERSION = "0.0.0.$REVISION_NUMER";
 
+
+
+sub notice($) {
+       my ($line) = @_;
+       print "[NOTICE] $line\n";
+};
+sub info($) {
+       my ($line) = @_;
+       print "[INFO] $line\n";
+};
+sub debug($) {
+       my ($line) = @_;
+       #print "[DEBUG] $line\n";
+};
+sub trace($) {
+       my ($line) = @_;
+       #print "[trace] $line\n";
+};
+sub trace2($) {
+       my ($line) = @_;
+       #print "[trace2] $line\n";
+};
+
+
+sub generate_config() {
+       notice("Error: \$LOGNAME is not set.\n") unless defined $ENV{'LOGNAME'};
+       my $gecos = defined $ENV{'LOGNAME'} ? (getpwnam($ENV{LOGNAME}))[6] : undef;
+       my $email;
+       my @keys;
+       my $hostname = `hostname -f`;
+       chomp $hostname;
+       my ($Cgecos,$Cemail,$Ckeys) = ('','','');
+       if (defined $gecos) {
+               $gecos =~ s/,.*//;
+
+               my $gpg = GnuPG::Interface->new();
+               $gpg->call( 'gpg' );
+               $gpg->options->hash_init(
+                       'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode } ] );
+               $gpg->options->meta_interactive( 0 );
+               my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
+               my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $gecos ]);
+               my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
+               waitpid $pid, 0;
+
+               if ($stdout eq '') {
+                       warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
+               };
+
+               @keys = ($stdout =~ /^pub:[^r:]*:(?:[^:]*:){2,2}([^:]+):/mg);
+               unless (scalar @keys) {
+                       info("Error: No keys were found using \"gpg --list-public-keys '$gecos'\".");
+                       @keys = qw{0123456789abcdef 89abcdef76543210};
+                       $Ckeys = '#';
+               }
+               ($email) =  ($stdout =~ /^uid:.*<(.+?@.+?)>.*:/m);
+               unless (defined $email) {
+                       info("Error: No email address was found using \"gpg --list-public-keys '$gecos'\".");
+                       $email = $ENV{'LOGNAME'}.'@'.$hostname;
+                       $Cemail = '#';
+               }
+       } else {
+               $gecos = 'Unknown Caff User';
+               $email = $ENV{'LOGNAME'}.'@'.$hostname;
+               @keys = qw{0123456789abcdef 89abcdef76543210};
+               ($Cgecos,$Cemail,$Ckeys) = ('#','#','#');
+       };
+
+       return <<EOT;
+# .caffrc -- vim:syntax=perl:
+# This file is in perl(1) format - see caff(1) for details.
+
+$Cgecos\$CONFIG{'owner'}       = '$gecos';
+$Cemail\$CONFIG{'email'}       = '$email';
+
+# you can get your long keyid from
+#   gpg --with-colons --list-key <yourkeyid|name|emailaddress..>
+#
+# if you have a v4 key, it will simply be the last 16 digits of
+# your fingerprint.
+#
+# Example:
+#   \$CONFIG{'keyid'}       = [ qw{FEDCBA9876543210} ];
+#  or, if you have more than one key:
+#   \$CONFIG{'keyid'}       = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
+
+$Ckeys\$CONFIG{'keyid'}       = [ qw{@keys} ];
+EOT
+};
+
 sub load_config() {
        my $config = $ENV{'HOME'} . '/.caffrc';
 sub load_config() {
        my $config = $ENV{'HOME'} . '/.caffrc';
-       -f $config or die "No file $config present.  See caff(1).\n";
+       unless (-f $config) {
+               print "No configfile $config present, I will use this template:\n";
+               my $template = generate_config();
+               print "$template\nPlease edit $config and run caff again.\n";
+               open F, ">$config" or die "$config: $!";
+               print F $template;
+               close F;
+               exit(1);
+       }
        unless (scalar eval `cat $config`) {
                die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
        };
 
        $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'};
        unless (scalar eval `cat $config`) {
                die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
        };
 
        $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'};
-       die ("owner is not defined.\n") unless defined $CONFIG{'owner'};
-       die ("email is not defined.\n") unless defined $CONFIG{'email'};
-       die ("keyid is not defined.\n") unless defined $CONFIG{'keyid'};
-       die ("keyid is not an array ref\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
+       die ("$PROGRAM_NAME: owner is not defined in $config.\n") unless defined $CONFIG{'owner'};
+       die ("$PROGRAM_NAME: email is not defined in $config.\n") unless defined $CONFIG{'email'};
+       die ("$PROGRAM_NAME: keyid is not defined in $config.\n") unless defined $CONFIG{'keyid'};
+       die ("$PROGRAM_NAME: keyid is not an array ref in $config.\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
        for my $keyid (@{$CONFIG{'keyid'}}) {
        for my $keyid (@{$CONFIG{'keyid'}}) {
-               $keyid =~ /^[A-F0-9]{16}$/i or die ("key $keyid is not a long (16 digit) keyid.\n");
+               $keyid =~ /^[A-F0-9]{16}$/i or die ("$PROGRAM_NAME: key $keyid is not a long (16 digit) keyid in $config.\n");
        };
        @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}};
        $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
        };
        @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}};
        $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
@@ -282,6 +405,7 @@ sub load_config() {
        $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
        $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
        $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
        $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
        $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
        $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
+       $CONFIG{'key-files'} = () unless defined $CONFIG{'key-files'};
        $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
 Hi,
 
        $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
 Hi,
 
@@ -306,27 +430,6 @@ Regards,
 EOM
 };
 
 EOM
 };
 
-sub notice($) {
-       my ($line) = @_;
-       print "[NOTICE] $line\n";
-};
-sub info($) {
-       my ($line) = @_;
-       print "[INFO] $line\n";
-};
-sub debug($) {
-       my ($line) = @_;
-       #print "[DEBUG] $line\n";
-};
-sub trace($) {
-       my ($line) = @_;
-       #print "[trace] $line\n";
-};
-sub trace2($) {
-       my ($line) = @_;
-       #print "[trace2] $line\n";
-};
-
 sub make_gpg_fds() {
        my %fds = (
                stdin => IO::Handle->new(),
 sub make_gpg_fds() {
        my %fds = (
                stdin => IO::Handle->new(),
@@ -439,8 +542,9 @@ sub readwrite_gpg($$$$$%) {
 sub ask($$;$$) {
        my ($question, $default, $forceyes, $forceno) = @_;
        my $answer;
 sub ask($$;$$) {
        my ($question, $default, $forceyes, $forceno) = @_;
        my $answer;
+       my $yn = $default ? '[Y/n]' : '[y/N]';
        while (1) {
        while (1) {
-               print $question,' ',($default ? '[Y/n]' : '[y/N]'), ' ';
+               print $question,' ',$yn, ' ';
                if ($forceyes && $forceno) {
                        print "$default (from config/command line)\n";
                        return $default;
                if ($forceyes && $forceno) {
                        print "$default (from config/command line)\n";
                        return $default;
@@ -455,9 +559,17 @@ sub ask($$;$$) {
                };
 
                $answer = <STDIN>;
                };
 
                $answer = <STDIN>;
+               if (!defined $answer) {
+                       $OUTPUT_AUTOFLUSH = 1;
+                       die "\n\n".
+                           "End of STDIN reached.  Are you using xargs?  Caff wants to read from STDIN,\n".
+                           "so you can't really use it with xargs.  A patch against caff to read from\n".
+                           "the terminal would be appreciated.\n".
+                           "For now instead of   cat keys | xargs caff  do  caff `cat keys`\n";
+               };
                chomp $answer;
                chomp $answer;
-               last if ((defined $answer) && (length $answer <= 1));
-               print "grrrrrr.\n";
+               last if ((length $answer == 0) || ($answer =~ m/^[yYnN]$/) );
+               print "What about $yn is so hard to understand?\nAnswer with either 'n' or 'y' or just press enter for the default.\n";
                sleep 1;
        };
        my $result = $default;
                sleep 1;
        };
        my $result = $default;
@@ -588,9 +700,9 @@ sub send_mail($$$@) {
                        Type        => "application/pgp-keys",
                        Disposition => 'attachment',
                        Encoding    => "7bit",
                        Type        => "application/pgp-keys",
                        Disposition => 'attachment',
                        Encoding    => "7bit",
-                       Description => "PGP Key 0x$key_id, uid ".($key->{'text'}).' ('.($key->{'serial'}).')',
+                       Description => "PGP Key 0x$key_id, uid ".($key->{'text'}).' ('.($key->{'serial'}).'), signed by 0x'.$CONFIG{'keyid'}[0],
                        Data        => $key->{'key'},
                        Data        => $key->{'key'},
-                       Filename    => "0x$key_id.".$key->{'serial'}.".asc");
+                       Filename    => "0x$key_id.".$key->{'serial'}.".signed-by-0x".$CONFIG{'keyid'}[0].".asc");
        };
 
        if ($can_encrypt) {
        };
 
        if ($can_encrypt) {
@@ -634,6 +746,7 @@ sub send_mail($$$@) {
        $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
        $message_entity->head->add("To", $address);
        $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
        $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
        $message_entity->head->add("To", $address);
        $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
+       $message_entity->head->add("Reply-To", $CONFIG{'reply-to'}) if defined $CONFIG{'reply-to'};
        $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
        $message_entity->head->add("User-Agent", $USER_AGENT);
        $message_entity->send();
        $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
        $message_entity->head->add("User-Agent", $USER_AGENT);
        $message_entity->send();
@@ -717,6 +830,7 @@ if (!GetOptions (
        '--no-download'   =>  \$params->{'no-download'},
        '-S'              =>  \$params->{'no-sign'},
        '--no-sign'       =>  \$params->{'no-sign'},
        '--no-download'   =>  \$params->{'no-download'},
        '-S'              =>  \$params->{'no-sign'},
        '--no-sign'       =>  \$params->{'no-sign'},
+       '--key-file=s@'   =>  \$params->{'key-files'},
        )) {
        usage(\*STDERR, 1);
 };
        )) {
        usage(\*STDERR, 1);
 };
@@ -758,6 +872,7 @@ $CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-downl
 $CONFIG{'no-mail'}     = $params->{'no-mail'}     if defined $params->{'no-mail'};
 $CONFIG{'mail'}        = $params->{'mail'}        if defined $params->{'mail'};
 $CONFIG{'no-sign'}     = $params->{'no-sign'}     if defined $params->{'no-sign'};
 $CONFIG{'no-mail'}     = $params->{'no-mail'}     if defined $params->{'no-mail'};
 $CONFIG{'mail'}        = $params->{'mail'}        if defined $params->{'mail'};
 $CONFIG{'no-sign'}     = $params->{'no-sign'}     if defined $params->{'no-sign'};
+push @{$CONFIG{'key-files'}}, @{$params->{'key-files'}} if defined $params->{'key-files'};
 
 
 #################
 
 
 #################
@@ -793,6 +908,24 @@ for my $keyid (@{$CONFIG{'keyid'}}) {
        }
 }
 
        }
 }
 
+########################
+# import keys from files
+########################
+foreach my $keyfile (@{$CONFIG{'key-files'}}) {
+    my $gpg = GnuPG::Interface->new();
+    $gpg->call( $CONFIG{'gpg'} );
+    $gpg->options->hash_init('homedir' => $GNUPGHOME);
+    $gpg->options->meta_interactive( 0 );
+    my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
+    my $pid = $gpg->import_keys(handles => $handles, command_args => $keyfile);
+    my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
+    info ("Importing keys from $keyfile");
+    waitpid $pid, 0;
+    if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
+       warn $stderr;
+    }
+}
+
 #############################
 # receive keys from keyserver
 #############################
 #############################
 # receive keys from keyserver
 #############################
@@ -818,6 +951,7 @@ if ($CONFIG{'no-download'}) {
 # [GNUPG:] NODATA 1
 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
        my %local_keyids = map { $_ => 1 } @KEYIDS;
 # [GNUPG:] NODATA 1
 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
        my %local_keyids = map { $_ => 1 } @KEYIDS;
+       my $had_v3_keys = 0;
        for my $line (split /\n/, $status) {
                if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
                        my $imported_key = $1;
        for my $line (split /\n/, $status) {
                if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
                        my $imported_key = $1;
@@ -836,12 +970,16 @@ if ($CONFIG{'no-download'}) {
                        delete $local_keyids{$speced_key};
                        unshift @keyids_ok, $imported_key;
                } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) {
                        delete $local_keyids{$speced_key};
                        unshift @keyids_ok, $imported_key;
                } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) {
+               } elsif ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{32})/) {
+                       my $imported_key = $1;
+                       notice ("Imported key $1 is a version 3 key.  Version 3 keys are obsolete, should not be used, and are not and will not be properly supported.");
+                       $had_v3_keys = 1;
                } else {
                        notice ("got unknown reply from gpg: $line");
                }
        };
        if (scalar %local_keyids) {
                } else {
                        notice ("got unknown reply from gpg: $line");
                }
        };
        if (scalar %local_keyids) {
-               notice ("Import failed for: ". (join ' ', keys %local_keyids).".");
+               notice ("Import failed for: ". (join ' ', keys %local_keyids)."." . ($had_v3_keys ? " (Or maybe it's one of those ugly v3 keys?)" :  ""));
                exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
        }
 };
                exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
        }
 };