note use of ~/.caff/gnupghome/gpg.conf in manpage
[pgp-tools.git] / caff / caff
index 1595a23b94f9dd65258fccfefb044f1ef0b51823..75ffcfd7e700cf098f5306e3cda34f1af2b0ae2e 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.
 
+=item B<--key-file> I<file>
+
+Import keys from file. Can be supplied more than once.
+
 =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/.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>.
+The file is generated when it does not exist.
 
 Example:
 
@@ -156,7 +169,7 @@ An additional keyid to encrypt messages to. Default: none.
 
 =item B<gpg-sign-args> [string]
 
-Additional arguments to pass to gpg. Default: none.
+Additional arguments to pass to gpg.  Default: none.
 
 =head2 Keyserver settings
 
@@ -169,12 +182,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
@@ -234,6 +256,10 @@ Default: none.
 
 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;
@@ -253,9 +279,62 @@ my $REVISION = '$Rev$';
 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
 my $VERSION = "0.0.0.$REVISION_NUMER";
 
+sub generate_config() {
+       die "Error: \$LOGNAME is not set.\n" unless $ENV{LOGNAME};
+       my $gecos = (getpwnam($ENV{LOGNAME}))[6];
+       $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.
+       };
+
+       my @keys;
+       unless (@keys = ($stdout =~ /^pub:[^r:]*:(?:[^:]*:){2,2}([^:]+):/mg)) {
+               die "Error: No keys were found using \"gpg --list-public-keys '$gecos'\".\n";
+       }
+       unless ($stdout =~ /^uid:.*<(.+@.+)>.*:/m) {
+               die "Error: No email address was found using \"gpg --list-public-keys '$gecos'\".\n";
+       }
+       my $email = $1;
+
+       return <<EOT;
+# .caffrc -- vim:syntax=perl:
+# This file is in perl(1) format - see caff(1) for details.
+
+\$CONFIG{'owner'}       = '$gecos';
+\$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.
+
+\$CONFIG{'keyid'}       = [ qw{@keys} ];
+EOT
+};
+
 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\nPress enter to continue.";
+               <STDIN>;
+               open F, ">$config" or die "$config: $!";
+               print F $template;
+               close F;
+       }
        unless (scalar eval `cat $config`) {
                die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
        };
@@ -266,7 +345,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'};
@@ -277,6 +356,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,
 
@@ -285,9 +365,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.
@@ -429,16 +513,35 @@ sub readwrite_gpg($$$$$%) {
 
 sub ask($$;$$) {
        my ($question, $default, $forceyes, $forceno) = @_;
-       return $default if $forceyes and $forceno;
-       return 1 if $forceyes;
-       return 0 if $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 appreciated.\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;
@@ -492,9 +595,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 ]);
@@ -512,7 +622,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);
@@ -560,9 +672,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) {
@@ -571,7 +683,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();
@@ -689,6 +801,7 @@ if (!GetOptions (
        '--no-download'   =>  \$params->{'no-download'},
        '-S'              =>  \$params->{'no-sign'},
        '--no-sign'       =>  \$params->{'no-sign'},
+       '--key-file=s@'   =>  \$params->{'key-files'},
        )) {
        usage(\*STDERR, 1);
 };
@@ -706,7 +819,7 @@ usage(\*STDERR, 1) unless scalar @ARGV >= 1;
 if ($params->{'local-user'}) {
        $USER = $params->{'local-user'};
        $USER =~ s/^0x//i;
-       unless ($USER =~ /^([A-Z0-9]{8}|[A-Z0-9]{16}|[A-Z0-9]{40})$/i) {
+       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);
        };
@@ -715,7 +828,11 @@ if ($params->{'local-user'}) {
 
 for my $keyid (@ARGV) {
        $keyid =~ s/^0x//i;
-       unless ($keyid =~ /^([A-Z0-9]{8}|[A-Z0-9]{16}||[A-Z0-9]{40})$/i) {
+       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);
        };
@@ -726,32 +843,59 @@ $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'};
+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
@@ -766,7 +910,7 @@ if ($CONFIG{'no-download'}) {
        $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 $pid = $gpg->recv_keys(handles => $handles, command_args => [ @KEYIDS ]);
@@ -814,6 +958,10 @@ unless (@keyids_ok) {
 ###########
 # 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) {
@@ -822,9 +970,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, split ' ', $CONFIG{'gpg-sign-args'} || "";
+               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);
        };
@@ -839,10 +989,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;
@@ -900,7 +1051,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' ],
@@ -1046,7 +1197,7 @@ for my $keyid (@keyids_ok) {
                        if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
                                my $address = $uid->{'text'};
                                $address =~ s/.*<(.*)>.*/$1/;
-                               if (ask("Send mail to '$address' for $uid->{'text'}?", 1, $CONFIG{'mail'})) {
+                               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";