Improve diagnosis if caff can't read answers from STDIN
[pgp-tools.git] / caff / caff
index 54bb9cc75ad44aaadc6b1f9d46d0e29eab6b9a7b..866af1919956fc271ddb86dc45418e4089aaef92 100755 (executable)
--- a/caff/caff
+++ b/caff/caff
@@ -40,7 +40,7 @@ caff -- CA - Fire and Forget
 
 =over
 
-=item B<caff> [-mMR] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
+=item B<caff> [-eEmMRS] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
 
 =back
 
@@ -56,6 +56,15 @@ sigs and sigs done by you.
 
 =over
 
+=item B<-e>, B<--export-old>
+
+Export old signatures. Default is to ask the user for each old signature.
+
+=item B<-E>, B<--no-export-old>
+
+Do not export old signatures. Default is to ask the user for each old
+signature.
+
 =item B<-m>, B<--mail>
 
 Send mail after signing. Default is to ask the user for each uid.
@@ -68,10 +77,18 @@ Do not send mail after signing. Default is to ask the user for each uid.
 
 Do not retrieve the key to be signed from a keyserver.
 
+=item B<-S>, B<--no-sign>
+
+Do not sign the keys.
+
 =item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
 
 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
@@ -141,6 +158,10 @@ Path to your secret keyring.  Default: B<$HOME/.gnupg/secring.gpg>.
 
 An additional keyid to encrypt messages to. Default: none.
 
+=item B<gpg-sign-args> [string]
+
+Additional arguments to pass to gpg.  Default: none.
+
 =head2 Keyserver settings
 
 =item B<keyserver> [string]
@@ -152,12 +173,21 @@ 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>.
 
+=item B<key-files> [list of files]
+
+A list of files containing keys to be imported.
+
 =head2 Signing settings
 
 =item B<no-sign> [boolean]
 
 If true, then skip the signing step. Default: B<0>.
 
+=item B<ask-sign> [boolean]
+
+If true, then pause before continuing to the signing step.
+This is useful for offline signing. Default: B<0>.
+
 =item B<export-sig-age> [seconds]
 
 Don't export UIDs by default, on which your latest signature is older
@@ -176,7 +206,7 @@ $CONFIG{caffhome}/keys/. Default: B<0>.
 
 =item B<mail-template> [string]
 
-Email template which is used as the body text for the email sent out.
+Email template which is used as the body text for the email sent out
 instead of the default text if specified. The following perl variables
 can be used in the template:
 
@@ -249,7 +279,7 @@ sub load_config() {
        die ("keyid is not defined.\n") unless defined $CONFIG{'keyid'};
        die ("keyid is not an array ref\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
        for my $keyid (@{$CONFIG{'keyid'}}) {
-               $keyid =~ /^[A-Fa-z0-9]{16}$/ or die ("key $keyid is not a long (16 digit) keyid.\n");
+               $keyid =~ /^[A-F0-9]{16}$/i or die ("key $keyid is not a long (16 digit) keyid.\n");
        };
        @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}};
        $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
@@ -260,6 +290,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{'key-files'} = () unless defined $CONFIG{'key-files'};
        $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
 Hi,
 
@@ -268,9 +299,13 @@ please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
     $OUT .= "\t".$uid."\n";
 };} of your key {$key} signed by me.
 
-Note that I did not upload your key to any keyservers. If you want this
-new signature to be available to others, please upload it yourself.
-With GnuPG this can be done using
+Note that I did not upload your key to any keyservers.
+If you have multiple user ids, I sent the signature for each user id
+separately to that user id's associated email address. You can import
+the signatures by running each through `gpg --import`.
+
+If you want this new signature to be available to others, please upload
+it yourself. With GnuPG this can be done using
        gpg --keyserver subkeys.pgp.net --send-key {$key}
 
 If you have any questions, don't hesitate to ask.
@@ -410,15 +445,37 @@ sub readwrite_gpg($$$$$%) {
        return ($stdout, $stderr, $status);
 };
 
-sub ask($$) {
-       my ($question, $default) = @_;
+sub ask($$;$$) {
+       my ($question, $default, $forceyes, $forceno) = @_;
        my $answer;
+       my $yn = $default ? '[Y/n]' : '[y/N]';
        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) {
+                       print "YES (from config/command line)\n";
+                       return 1;
+               };
+               if ($forceno) {
+                       print "NO (from config/command line)\n";
+                       return 0;
+               };
+
                $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 apprechiated.\n".
+                           "For now instead of   cat keys | xargs caff  do  caff `cat keys`\n";
+               };
                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;
@@ -459,7 +516,7 @@ sub version($) {
 sub usage($$) {
        my ($fd, $exitcode) = @_;
        version($fd);
-       print $fd "Usage: $PROGRAM_NAME [-mMR] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
+       print $fd "Usage: $PROGRAM_NAME [-eEmMRS] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
        print $fd "Consult the manual page for more information.\n";
        exit $exitcode;
 };
@@ -472,9 +529,16 @@ sub export_key($$) {
 
        my $gpg = GnuPG::Interface->new();
        $gpg->call( $CONFIG{'gpg'} );
-       $gpg->options->hash_init(
-               'homedir' => $gnupghome,
-               'armor' => 1 );
+       if (defined $gnupghome) {
+               $gpg->options->hash_init(
+                       'homedir' => $gnupghome,
+                       'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ],
+                       'armor' => 1 );
+       } else {
+               $gpg->options->hash_init(
+                       'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ],
+                       'armor' => 1 );
+       };
        $gpg->options->meta_interactive( 0 );
        my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
        my $pid = $gpg->export_keys(handles => $handles, command_args => [ $keyid ]);
@@ -492,7 +556,9 @@ sub import_key($$) {
 
        my $gpg = GnuPG::Interface->new();
        $gpg->call( $CONFIG{'gpg'} );
-       $gpg->options->hash_init( 'homedir' => $gnupghome );
+       $gpg->options->hash_init(
+               'homedir' => $gnupghome,
+               'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ] );
        $gpg->options->meta_interactive( 0 );
        my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
        my $pid = $gpg->import_keys(handles => $handles);
@@ -540,9 +606,9 @@ sub send_mail($$$@) {
                        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'},
-                       Filename    => "0x$key_id.".$key->{'serial'}.".asc");
+                       Filename    => "0x$key_id.".$key->{'serial'}.".signed-by-0x".$CONFIG{'keyid'}[0].".asc");
        };
 
        if ($can_encrypt) {
@@ -551,7 +617,7 @@ sub send_mail($$$@) {
                my $gpg = GnuPG::Interface->new();
                $gpg->call( $CONFIG{'gpg'} );
                $gpg->options->hash_init( 'homedir' => $GNUPGHOME,
-                       'extra_args' => '--always-trust',
+                       'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ],
                        'armor' => 1 );
                $gpg->options->meta_interactive( 0 );
                my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
@@ -657,12 +723,19 @@ if (!GetOptions (
        '-V'              =>  \$params->{'version'},
        '-u=s'            =>  \$params->{'local-user'},
        '--local-user=s'  =>  \$params->{'local-user'},
+       '-e'              =>  \$params->{'export-old'},
+       '--export-old'    =>  \$params->{'export-old'},
+       '-E'              =>  \$params->{'no-export-old'},
+       '--no-export-old' =>  \$params->{'no-export-old'},
        '-m'              =>  \$params->{'mail'},
        '--mail'          =>  \$params->{'mail'},
        '-M'              =>  \$params->{'no-mail'},
        '--no-mail'       =>  \$params->{'no-mail'},
        '-R'              =>  \$params->{'no-download'},
        '--no-download'   =>  \$params->{'no-download'},
+       '-S'              =>  \$params->{'no-sign'},
+       '--no-sign'       =>  \$params->{'no-sign'},
+       '--key-file=s@'   =>  \$params->{'key-files'},
        )) {
        usage(\*STDERR, 1);
 };
@@ -680,7 +753,7 @@ usage(\*STDERR, 1) unless scalar @ARGV >= 1;
 if ($params->{'local-user'}) {
        $USER = $params->{'local-user'};
        $USER =~ s/^0x//i;
-       unless ($USER =~ /^[A-Za-z0-9]{8,8}([A-Za-z0-9]{8})?$/) {
+       unless ($USER =~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
                print STDERR "-u $USER is not a keyid.\n";
                usage(\*STDERR, 1);
        };
@@ -689,7 +762,11 @@ if ($params->{'local-user'}) {
 
 for my $keyid (@ARGV) {
        $keyid =~ s/^0x//i;
-       unless ($keyid =~ /^[A-Za-z0-9]{8}([A-Za-z0-9]{8}|[A-Za-z0-9]{32})?$/) {
+       unless ($keyid =~ /^([A-F0-9]{8}|[A-F0-9]{16}||[A-F0-9]{40})$/i) {
+               if ($keyid =~ /^[A-F0-9]{32}$/) {
+                       info("Ignoring v3 fingerprint $keyid.  v3 keys are obsolete.");
+                       next;
+               };
                print STDERR "$keyid is not a keyid.\n";
                usage(\*STDERR, 1);
        };
@@ -699,94 +776,126 @@ for my $keyid (@ARGV) {
 $CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
 $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'};
 
 
 #################
 # import own keys
 #################
+for my $keyid (@{$CONFIG{'keyid'}}) {
        my $gpg = GnuPG::Interface->new();
        $gpg->call( $CONFIG{'gpg'} );
        $gpg->options->hash_init(
                'homedir' => $GNUPGHOME,
-               'extra_args' => '--keyserver='.$CONFIG{'keyserver'} );
+               'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode --fast-list-mode } ] );
        $gpg->options->meta_interactive( 0 );
        my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
-       $gpg->options->hash_init( 'extra_args' => [ '--with-colons', '--fixed-list-mode' ] );
-       my $pid = $gpg->list_public_keys(handles => $handles, command_args => $CONFIG{'keyid'});
+       my $pid = $gpg->list_public_keys(handles => $handles, command_args => $keyid);
        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");
-               next;
+               warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
        };
-       foreach my $keyid (@{$CONFIG{'keyid'}}) {
-               unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
-                       info("Importing $keyid");
-                       system "gpg --export $keyid | gpg --import --homedir $GNUPGHOME";
-               }
+       unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
+               info("Key $keyid not found in caff's home.  Getting it from your normal GnuPGHome.");
+               my $key = export_key(undef, $keyid);
+               if (!defined $key || $key eq '') {
+                       warn ("Did not get key $keyid from your normal GnuPGHome\n");
+                       next;
+               };
+               my $result = import_key($GNUPGHOME, $key);
+               unless ($result) {
+                       warn ("Could not import $keyid into caff's gnupghome.\n");
+                       next;
+               };
        }
+}
+
+########################
+# 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
 #############################
 my @keyids_ok;
-my @keyids_failed;
 if ($CONFIG{'no-download'}) {
        @keyids_ok = @KEYIDS;
 } else {
+       info ("fetching keys, this will take a while...");
+
        my $gpg = GnuPG::Interface->new();
        $gpg->call( $CONFIG{'gpg'} );
        $gpg->options->hash_init(
                'homedir' => $GNUPGHOME,
-               'extra_args' => '--keyserver='.$CONFIG{'keyserver'} );
+               'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always }, '--keyserver='.$CONFIG{'keyserver'} ] );
        $gpg->options->meta_interactive( 0 );
        my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
-       
-       my @local_keyids = @KEYIDS;
-       for my $keyid (@local_keyids) {
-               info ("fetching $keyid...");
-               my $pid = $gpg->recv_keys(handles => $handles, command_args => [ $keyid ]);
-               my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
-               waitpid $pid, 0;
+       my $pid = $gpg->recv_keys(handles => $handles, command_args => [ @KEYIDS ]);
+       my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
+       waitpid $pid, 0;
 
 # [GNUPG:] IMPORT_OK 0 5B00C96D5D54AEE1206BAF84DE7AAF6E94C09C7F
 # [GNUPG:] NODATA 1
 # [GNUPG:] NODATA 1
 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
-               my $handled = 0;
-               for my $line (split /\n/, $status) {
-                       if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
-                               my $imported_key = $1;
-                               if ($keyid ne $imported_key &&
-                                   $keyid ne substr($imported_key, -16) &&
-                                   $keyid ne substr($imported_key, -8)) {
-                                   warn("Imported unexpected key.  expected: $keyid; got: $imported_key.\n");
-                                   next;
-                               };
-                               push @keyids_ok, $keyid;
-                               shift @KEYIDS;
-                               $handled = 1;
-                               last;
-                       } elsif ($line =~ /^\[GNUPG:\] NODATA/) {
-                               push @keyids_failed, $keyid;
-                               shift @KEYIDS;
-                               $handled = 1;
-                               last;
+       my %local_keyids = map { $_ => 1 } @KEYIDS;
+       for my $line (split /\n/, $status) {
+               if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
+                       my $imported_key = $1;
+                       my $whole_fpr = $imported_key;
+                       my $long_keyid = substr($imported_key, -16);
+                       my $short_keyid = substr($imported_key, -8);
+                       my $speced_key;
+                       for my $spec (($whole_fpr, $long_keyid, $short_keyid)) {
+                               $speced_key = $spec if $local_keyids{$spec};
                        };
-               };
-               unless ($handled) {
-                       notice ("Huh, what's up with $keyid?");
-                       push @keyids_failed, $keyid;
-                       shift @KEYIDS;
-               };
+                       unless ($speced_key) {
+                           notice ("Imported unexpected key; got: $imported_key\n");
+                           next;
+                       };
+                       debug ("Imported $imported_key for $speced_key");
+                       delete $local_keyids{$speced_key};
+                       unshift @keyids_ok, $imported_key;
+               } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) {
+               } else {
+                       notice ("got unknown reply from gpg: $line");
+               }
        };
-       die ("Still keys in \@KEYIDS.  This should not happen.") if scalar @KEYIDS;
-       notice ("Import failed for: ". (join ' ', @keyids_failed).".") if scalar @keyids_failed;
+       if (scalar %local_keyids) {
+               notice ("Import failed for: ". (join ' ', keys %local_keyids).".");
+               exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
+       }
 };
 
+unless (@keyids_ok) {
+       notice ("No keys to sign found");
+       exit 0;
+}
+
 ###########
 # sign keys
 ###########
+if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) {
+       $CONFIG{'no-sign'} = ! ask("Continue with signing?", 1);
+}
+       
 unless ($CONFIG{'no-sign'}) {
        info("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
        for my $keyid (@keyids_ok) {
@@ -795,8 +904,11 @@ unless ($CONFIG{'no-sign'}) {
                push @command, '--local-user', $USER if (defined $USER);
                push @command, "--homedir=$GNUPGHOME";
                push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
+               push @command, '--no-auto-check-trustdb';
+               push @command, '--trust-model=always';
                push @command, '--edit', $keyid;
                push @command, 'sign';
+               push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
                print join(' ', @command),"\n";
                system (@command);
        };
@@ -811,10 +923,11 @@ for my $keyid (@keyids_ok) {
        #################
        my $gpg = GnuPG::Interface->new();
        $gpg->call( $CONFIG{'gpg'} );
-       $gpg->options->hash_init( 'homedir' => $GNUPGHOME );
+       $gpg->options->hash_init(
+               'homedir' => $GNUPGHOME,
+               '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();
-       $gpg->options->hash_init( 'extra_args' => [ '--with-colons', '--fixed-list-mode' ] );
        my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $keyid ]);
        my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
        waitpid $pid, 0;
@@ -872,7 +985,7 @@ for my $keyid (@keyids_ok) {
                $gpg->call( $CONFIG{'gpg-delsig'} );
                $gpg->options->hash_init(
                        'homedir' => $tempdir,
-                       'extra_args' => [ '--with-colons', '--fixed-list-mode', '--command-fd=0', '--no-tty' ] );
+                       'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode --command-fd=0 --no-tty } ] );
                ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
                $pid = $gpg->wrap_call(
                        commands     => [ '--edit' ],
@@ -977,7 +1090,7 @@ for my $keyid (@keyids_ok) {
 
                if ($signed_by_me) {
                        if ($NOW - $signed_by_me > $CONFIG{'export-sig-age'} ) {
-                               my $write = ask("Signature on $this_uid_text is old.  Export?", 0);
+                               my $write = ask("Signature on $this_uid_text is old.  Export?", 0, $params->{'export-old'}, $params->{'no-export-old'});
                                next unless $write;
                        };
                        my $keydir = "$KEYSBASE/$DATE_STRING";
@@ -1018,7 +1131,7 @@ for my $keyid (@keyids_ok) {
                        if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
                                my $address = $uid->{'text'};
                                $address =~ s/.*<(.*)>.*/$1/;
-                               if ($CONFIG{'mail'} or ask("Send mail to '$address' for $uid->{'text'}?", 1)) {
+                               if (ask("Mail signature for $uid->{'text'} to '$address'?", 1, $CONFIG{'mail'})) {
                                        my $mail = send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
 
                                        my $keydir = "$KEYSBASE/$DATE_STRING";