1#!/usr/local/bin/perl
2
3# caff  --  CA - Fire and Forget
4#
5# Copyright (c) 2004, 2005, 2006 Peter Palfrader <peter@palfrader.org>
6# Copyright (c) 2005, 2006 Christoph Berg <cb@df7cb.de>
7# Copyright (c) 2014-2016 Guilhem Moulin <guilhem@debian.org>
8#
9# All rights reserved.
10#
11# Redistribution and use in source and binary forms, with or without
12# modification, are permitted provided that the following conditions
13# are met:
14# 1. Redistributions of source code must retain the above copyright
15#    notice, this list of conditions and the following disclaimer.
16# 2. Redistributions in binary form must reproduce the above copyright
17#    notice, this list of conditions and the following disclaimer in the
18#    documentation and/or other materials provided with the distribution.
19# 3. The name of the author may not be used to endorse or promote products
20#    derived from this software without specific prior written permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
23# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
24# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
25# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
26# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
27# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
33=pod
34
35=head1 NAME
36
37caff -- CA - Fire and Forget
38
39=head1 SYNOPSIS
40
41=over
42
43=item B<caff> [-eERS] [-m I<yes|ask-yes|ask-no|no>] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
44
45=item B<caff> [-eERS] [-m I<yes|ask-yes|ask-no|no>] [-u I<yourkeyid>] [I<keyid> ..] <I</path/to/ksp-annotated.txt>
46
47=back
48
49=head1 DESCRIPTION
50
51CA Fire and Forget is a script that helps you in keysigning.  It takes a list
52of keyids on the command line, fetches them from a keyserver and calls GnuPG so
53that you can sign it.  It then mails each key to all its email addresses - only
54including the one UID that we send to in each mail, pruned from all but self
55sigs and sigs done by you.  The mailed key is encrypted with itself as a means
56to verify that key belongs to the recipient.
57
58The list of keys to sign can also be provided through caff's standard
59input, as gpgparticipants(1) formatted content.  Only keys for which
60both the "Fingerprint OK" and "ID OK" boxes are ticked (i.e., marked
61with an "x") are considered for signing.  Furthermore, the input header
62must include at least one checksum line, and all checksum boxes must be
63marked as verified (with an "x").
64
65=head1 OPTIONS
66
67=over
68
69=item B<-e>, B<--export-old>
70
71Export old signatures. Default is to ask the user for each old signature.
72
73=item B<-E>, B<--no-export-old>
74
75Do not export old signatures. Default is to ask the user for each old
76signature.
77
78=item B<-m>, B<--mail> I<yes|ask-yes|ask-no|no>
79
80Whether to send mail after signing. Default is to ask, for each uid,
81with a default value of yes.
82
83=item B<-R>, B<--no-download>
84
85Do not retrieve the key to be signed from a keyserver.
86
87=item B<-S>, B<--no-sign>
88
89Do not sign the keys.
90
91=item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
92
93Select the key that is used for signing, in case you have more than one key.
94To sign with multiple keys at once, separate multiple keyids by comma. This
95option requires the key(s) to be defined through the keyid variable in the
96configuration file.
97
98=item B<--key-file> I<file>
99
100Import keys from file. Can be supplied more than once.
101
102=item B<--keys-from-gnupg>
103
104Try to import keys from your standard GnuPG keyrings.
105
106=item B<--debug>
107
108Enable debug messages.
109
110=back
111
112=head1 ENVIRONMENT
113
114=over
115
116=item I<HOME>
117
118The default home directory.
119
120=item I<GNUPGBIN>
121
122The gpg binary.  Default: C<"gpg">.
123
124=item I<GNUPGHOME>
125
126The default working directory for gpg.  Default: C<$HOME/.gnupg>.
127
128=back
129
130=head1 FILES
131
132=over
133
134=item $HOME/.caffrc  -  configuration file
135
136=item $HOME/.caff/keys/yyyy-mm-dd/  -  processed keys
137
138=item $HOME/.caff/gnupghome/  -  caff's working directory for gpg
139
140=item $HOME/.caff/gnupghome/gpg.conf  -  gpg configuration (see B<NOTES> below)
141
142useful options include use-agent, keyserver, keyserver-options, default-cert-level, etc.
143
144=back
145
146=head1 CONFIGURATION FILE OPTIONS
147
148The configuration file is a perl script that sets values in the hash B<%CONFIG>.
149The file is generated when it does not exist.
150
151Example:
152
153	$CONFIG{'owner'} = q{Peter Palfrader};
154	$CONFIG{'email'} = q{peter@palfrader.org};
155	$CONFIG{'keyid'} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
156
157=head2 Required basic settings
158
159=over
160
161=item B<owner> [string]
162
163Your name.  B<REQUIRED>.
164
165=item B<email> [string]
166
167Your email address, used in From: lines.  B<REQUIRED>.
168
169=item B<keyid> [list of keyids]
170
171A list of your keys.  This is used to determine which signatures to keep
172in the pruning step.  If you select a key using B<-u> it has to be in
173this list.  B<REQUIRED>.
174
175=back
176
177=head2 General settings
178
179=over
180
181=item B<caffhome> [string]
182
183Base directory for the files caff stores.  Default: B<$HOME/.caff/>.
184
185=item B<colors> [hash]
186
187How to color output messages.  See the C<Term::ANSIColor> documentation
188for the list of supported colors; colored output can be disabled by
189setting this option to an empty hash B<{}>.  Default:
190
191	{ error => 'bold bright_red'
192	, warn => 'bright_red'
193	, notice => 'bold'
194	, info => ''
195	, success => 'green' # used in combination with 'notice' and 'info'
196	, fail => 'yellow'   # used in combination with 'notice' and 'info'
197	}
198
199=back
200
201=head2 GnuPG settings
202
203=over
204
205=item B<gpg> [string]
206
207Path to the GnuPG binary.  Default: The value of the I<GNUPGBIN>
208environment variable if set, otherwise C<gpg>.
209
210=item B<secret-keyring> [string]
211
212Path to your secret keyring (GnuPG < 2.1), or to the GnuPGHOME
213of the agent managing the secret key material (GnuPG >= 2.1).
214Default: B<$HOME/.gnupg/secring.gpg>.
215If the value is not a directory with GnuPG >= 2.1, the parent directory
216(i.e., B<$HOME/.gnupg> by default) is considered instead.
217
218=item B<also-encrypt-to> [keyid, or list of keyids]
219
220Additional keyids to encrypt messages to. Default: none.
221
222=item B<gpg-sign-type> [string]
223
224The prefix to the "sign" command used to make the signature from gpg's
225shell.  Can be set to a mix of "l" (local), "nr" (non-revocable) or "t"
226(trust) to make a signature of the given type.  See gpg(1) for
227details.  Default: "" (i.e., make a regular, exportable, signature).
228
229=item B<gpg-sign-args> [string]
230
231Additional commands to pass to gpg after the "sign" command.
232Default: none.
233
234=back
235
236=head2 Key import settings
237
238=over
239
240=item B<no-download> [boolean]
241
242If true, then skip the step of fetching keys from the keyserver.
243Default: B<0>.
244
245=item B<key-files> [list of files]
246
247A list of files containing keys to be imported.
248
249=back
250
251=head2 Signing settings
252
253=over
254
255=item B<no-sign> [boolean]
256
257If true, then skip the signing step. Default: B<0>.
258
259=item B<ask-sign> [boolean]
260
261If true, then pause before continuing to the signing step.
262This is useful for offline signing. Default: B<0>.
263
264=item B<export-sig-age> [seconds]
265
266Don't export UIDs by default, on which your latest signature is older
267than this age.  Default: B<24*60*60> (i.e. one day).
268
269=item B<local-user> [keyid, or list of keyids]
270
271Select the key that is used for signing, in case you have more than one key.
272With multiple keyids, sign with each key in turn.
273
274=item B<also-lsign-in-gnupghome> [auto|ask|no]
275
276Whether to locally sign the UIDs in the user's GnuPGHOME, in addition to
277caff's signatures in its own GnuPGHOME.  Such signatures are not
278exportable.  This can be useful when the recipient forgets to upload the
279signatures caff sent (or if they are non-exportable as well), as it
280gives a way to keep track of which UIDs were verified.  However, note
281that local signatures will not be deleted once the recipient does the
282upload and the signer refreshes her keyring.
283
284If the value is not I<no> and if B<gpg-sign-type> contains "l", each
285(local) signature is merely exported from caff's own GnuPGHOME to the
286user's.  Otherwise, if the value is I<auto>, each UID signed in caff's
287own GnuPGHOME gets automatically locally signed in the user's, using the
288same certification level; this requires a working gpg-agent(1).  If
289I<ask>, the user is prompted for which UIDs to locally sign.  Default:
290B<no>.
291
292=item B<show-photos> [boolean]
293
294If true, then before signing a key gpg will display the photos attached
295to it, if any.  (The photo viewer can be specified with a "photo-viewer"
296option in caff's GnuPGHOME.)  Default: B<0>.
297
298=back
299
300=head2 Mail settings
301
302=over
303
304=item B<mail> [yes|ask-yes|ask-no|no]
305
306Whether to send mails. This is a quad-option, with which you can set the
307behaviour: yes always sends, no never sends; ask-yes and ask-no asks, for
308each uid, with according defaults for the question. Default: B<ask-yes>.
309
310In any case, the messages are also written to $CONFIG{'caffhome'}/keys/
311
312=item B<mail-cant-encrypt> [yes|ask-yes|ask-no|no]
313
314The value of this option is considered instead of that of B<mail> for
315recipient keys without encryption capability.  Default to the value of
316B<mail>.
317
318=item B<mail-subject> [string]
319
320Sets the value of the "Subject:" header field.  C<%k> will be expanded
321to the long key ID of the signed key.
322Default: C<Your signed PGP key 0x%k>.
323
324=item B<mail-template> [string]
325
326Email template which is used as the body text for the email sent out
327instead of the default text if specified. The following perl variables
328can be used in the template:
329
330=over
331
332=item B<{owner}> [string]
333
334Your name as specified in the L<B<owner>|/item_owner__5bstring_5d> setting.
335
336=item B<{key}> [string]
337
338The keyid of the key you signed.
339
340=item B<{@uids}> [array]
341
342The UIDs for which signatures are included in the mail.
343
344=back
345
346Note that you should probably customize the template if you intend to
347send non-exportable signatures (i.e., if B<gpg-sign-type> contains "l"),
348as uploading such signatures doesn't make sense, and they require the
349import option "import-local-sigs" which isn't set by default.
350
351=item B<reply-to> [string]
352
353Add a Reply-To: header to messages sent. Default: none.
354
355=item B<bcc> [string]
356
357Address to send blind carbon copies to when sending mail.
358Default: none.
359
360=item B<mailer-send> [array]
361
362Parameters to pass to Mail::Mailer.  Default: none.
363Setting this option is strongly discouraged: fix your local MTA instead.
364
365This could for example be
366
367	$CONFIG{'mailer-send'} =  [ 'smtp', Server => 'mail.server', Auth => ['user', 'pass'] ];
368
369to use the perl SMTP client, or
370
371	$CONFIG{'mailer-send'} =  [ 'sendmail', '-f', $CONFIG{'email'}, '-it' ];
372
373to pass arguments to the sendmail program.  To specify a sendmail binary
374you can set the C<< PERL_MAILERS >> environment variable as follows:
375
376    $ENV{'PERL_MAILERS'} = 'sendmail:/path/to/sendmail_compatible_mta';
377
378For more information see Mail::Mailer(3pm).
379
380=back
381
382=head1 NOTES
383
384As noted above caff uses its own GnuPGHOME and GnuPG configuration file.
385In fact it only needs its own keyring for the signing work, but it would
386be unsafe to reuse the same GnuPG configuration file because the user
387could have set an option in $HOME/.gnupg/gpg.conf which would break caff.
388
389Therefore the GnuPG options that are intended to be used with caff, such
390as C<keyserver> or C<cert-digest-algo>, need to be placed in
391$HOME/.caff/gnupghome/gpg.conf instead.  If this file does not exist,
392the GnuPG options found in $HOME/.gnupg/gpg.conf that are known to be
393safe (and useful) for caff, are passed to gpg(1) as command-line
394options.
395
396=head1 AUTHORS
397
398=over
399
400=item Peter Palfrader <peter@palfrader.org>
401
402=item Christoph Berg <cb@df7cb.de>
403
404=item Guilhem Moulin <guilhem@debian.org>
405
406=back
407
408=head1 SEE ALSO
409
410gpg(1), pgp-clean(1), /usr/share/doc/signing-party/caff/
411
412=cut
413
414use strict;
415use warnings;
416use IO::Handle;
417use File::Copy qw{copy};
418use File::Temp;
419use Text::Template;
420use MIME::Entity;
421use Encode ();
422use I18N::Langinfo qw{langinfo};
423use Net::IDN::Encode qw{email_to_ascii domain_to_ascii};
424use Fcntl;
425use IO::Select;
426use Getopt::Long;
427use GnuPG::Interface;
428use POSIX qw{strftime setlocale};
429use Term::ANSIColor qw{colored};
430
431my %CONFIG;
432my $VERSION = '@@VERSION@@';
433my $LOCALE = Encode::find_encoding(langinfo(I18N::Langinfo::CODESET()));
434my $USER_AGENT = "caff $VERSION";
435
436# Global variables
437my @KEYIDS;
438my @LOCAL_USER;
439my $PARAMS;
440
441my $KEYSBASE;
442my $GNUPGHOME;
443
444
445##
446# Display an error message on STDERR and then exit.
447#
448# @param $exitcode exit code status to use to end the program
449# @param $line     error message to display on STDERR
450#
451sub mycolored($@) {
452    my $msg = shift;
453    my $color = join (' ', grep defined, map { defined $_ ? $CONFIG{colors}->{$_} : undef } @_) if defined $CONFIG{colors};
454    $msg = colored($msg, $color) if defined $color and $color !~ /^\s*$/;
455    return $msg;
456}
457sub myerror($$) {
458    my ($exitcode, $line) = @_;
459    print STDERR mycolored("[ERROR] $line", 'error'), "\n";
460    exit $exitcode;
461}
462
463sub mywarn($) {
464    my ($line) = @_;
465    print STDERR mycolored("[WARN] $line", 'warn'), "\n";
466}
467sub notice($;$) {
468    my ($line,$color) = @_;
469    $color = $color ? 'success' : 'fail' if defined $color;
470    print STDERR mycolored("[NOTICE] $line", 'notice', $color), "\n";
471}
472sub info($;$) {
473    my ($line,$color) = @_;
474    $color = $color ? 'success' : 'fail' if defined $color;
475    print STDERR mycolored("[INFO] $line", 'info', $color), "\n";
476}
477sub debug($) {
478    my ($line) = @_;
479    print STDERR "[DEBUG] $line\n" if $PARAMS->{debug};
480}
481sub trace($) {
482    my ($line) = @_;
483    #print STDERR "[trace] $line\n";
484}
485sub trace2($) {
486    my ($line) = @_;
487    #print STDERR "[trace2] $line\n";
488}
489
490sub mysystem(@) {
491    system { $_[0] } @_;
492    myerror($?, "$_[0] exited with value ".($? >> 8)) if $?;
493}
494
495
496# Return -1 if the GnuPG version is < $_[0], 0 if == $_[0], 1 if > $_[0].
497my $GNUPG_VERSION;
498sub GnuPG_version($) {
499    unless (defined $GNUPG_VERSION) { # cache the version
500        $GNUPG_VERSION = `$CONFIG{gpg} --no-options --with-colons --list-config version` or exit 1;
501        chomp $GNUPG_VERSION;
502        $GNUPG_VERSION =~ s/^cfg:version:// or die;
503        debug "gpg (GnuPG) $GNUPG_VERSION";
504    }
505    my @v1 = split /\./, $GNUPG_VERSION;
506    my @v2 = split /\./, shift;
507    while (@v1 or @v2) {
508        my $v1 = shift @v1 // 0;
509        my $v2 = shift @v2 // 0;
510        my $r = $v1 <=> $v2;
511        return $r unless $r == 0;
512    }
513    return 0;
514}
515
516sub gpgconf(@) {
517    my $pid = open my $fh, '-|', 'gpgconf', @_;
518    my %conf;
519    while (<$fh>) {
520        my ($k, $v) = split /:/, $_;
521        chomp ($conf{$k} = $v);
522        $conf{$k} =~ s/%(\p{AHex}{2})/ chr(hex($1)) /ge; # unescape the %-encoded chars
523    }
524    waitpid $pid, 0;
525    myerror($?, "gpgconf exited with value ".($? >> 8)) if $? > 0;
526    close $fh;
527    return \%conf;
528}
529
530# See RFC 5322 section 3.4.1; only the pattern for the local part, which
531# doesn't go beyond the ASCII range, is validated.  The domain part is
532# NOT checked against RFC 5322, as it must be encoded to ASCII first;
533# for now any string in the full-range unicode that does not contain
534# U+0040 (commercial at), U+FE6B (small commercial at) and U+FF20
535# (fullwidth commercial at) is accepted.
536my $RE_word = qr/[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x41-\x5A\x5E-\x7E]+ # atom: any ASCII CHAR except specials, SPACE and CTLs
537                 |\x22(?:[\x00-\x21\x23-\x5B\x5D-\x7E]|\x5C\p{ASCII})*\x22       # quoted string
538                /x;
539my $RE_address_spec = qr/(?<l>$RE_word(?:\.$RE_word)*)[\@\N{U+FE6B}\N{U+FF20}](?<d>[^\@\N{U+FE6B}\N{U+FF20}]+)/o;
540
541# A domain label is a non-empty ASCII string of length at most 63
542# characters (RFC 1035 2.3.4).  Valid characters are alphanumeric and
543# hyphen '-', but an hyphen may not appear at the start or end of a
544# label (RFC 952, RFC 1123 2.1).
545my $RE_label = qr/[0-9a-z](?:[0-9a-z\x2D]{0,61}[0-9a-z])?/aai;
546
547# Take a 'mailbox' (RFC 5322 section 3.4) and return its ASCII-encoded
548# 'addr-spec'; or undef if it violates one of RFC 5322/5892/1035/5321.
549# We're not using Email::Valid because it's not unicode-friendly.
550# NOTE: This subroutine should only be used to extract e-mail addresses
551# from UIDs.  The phrase is NOT checked against RFC 5322 (any string
552# containing only characters in the full-unicode printable range are
553# accepted), but we don't care as long as it's not used in email
554# headers.
555sub email_valid($) {
556    local $_ = shift // return;
557    return unless /\A$RE_address_spec\z/ao or                         # addr-spec
558                  /\A(?:\p{Print}*\p{Space})?<$RE_address_spec>\z/ao; # [phrase] "<" addr-spec ">"
559    my ($l,$d) = @+{qw/l d/};
560    if ($d =~ /\P{ASCII}/) {
561        # encode the IDN to ASCII using Punycode for RFC 5321 validation
562        eval { $d = domain_to_ascii($d) };
563        return if $@; # violates RFC 5892
564    }
565    my $address = "$l\@$d";
566    return unless
567        length $d > 0 and length $d <= 255 # violates RFC 1035 2.3.4 "size limits"
568        and length $l <= 64                # violates RFC 5321 4.5.3.1.1
569        and length $address <= 254         # violates RFC 5321 4.5.3.1.3
570        and $d =~ /\A$RE_label(?:\.$RE_label)+\z/o; # ignore non-FQDN
571    return $address;
572}
573
574open NULL, '+<', '/dev/null';
575my $NULL = fileno NULL;
576sub generate_config() {
577    notice("Error: \$LOGNAME is not set", 0) unless defined $ENV{'LOGNAME'};
578    my $gecos = defined $ENV{'LOGNAME'} ? (getpwnam($ENV{LOGNAME}))[6] : undef;
579    my $email;
580    my @keys;
581    # BSD does not have hostname -f, so we try without -f first
582    my $hostname = `hostname`;
583    $hostname = `hostname -f` unless $hostname =~ /\./;
584    chomp $hostname;
585    my ($Cgecos,$Cemail,$Ckeys) = ('','','');
586
587    if (defined $gecos) {
588        $gecos =~ s/,.*//;
589
590        $CONFIG{'gpg'} = $ENV{GNUPGBIN} // 'gpg';
591        my $gpg = mkGnuPG( extra_args => ['--with-colons'] );
592        my $handles = mkGnuPG_fds ( stdout => undef );
593        my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $gecos ]);
594        my %output = readwrite_gpg($handles);
595        waitpid $pid, 0;
596        $handles->{stdout}->close;
597
598        if ($output{stdout} eq '') {
599            mywarn "No data from $CONFIG{gpg} for list-key"; # There should be at least 'tru:' everywhere.
600        };
601
602        @keys = ($output{stdout} =~ /^pub:[^eir:]*:(?:[^:]*:){2}([0-9A-F]{16}):/mg);
603        unless (scalar @keys) {
604            notice("Error: No keys were found using \"$CONFIG{gpg} --list-public-keys '$gecos'\"", 0);
605            @keys = qw{0123456789ABCDEF 89ABCDEF76543210};
606            $Ckeys = '#';
607        }
608        my @emails = ($output{stdout} =~ /^uid:[^eir:]*:(?:[^:]*:){7}([^:]+)(?::.*)?$/mg);
609        if (@emails) {
610            s/\\x(\p{AHex}{2})/ chr(hex($1)) /ge foreach @emails;
611            @emails = grep defined, map {email_valid(Encode::decode_utf8($_))} @emails;
612            $email = shift @emails; # take the first valid address
613        }
614        unless (defined $email) {
615            notice("Error: No email address was found using \"$CONFIG{gpg} --list-public-keys '$gecos'\"", 0);
616            $email = $ENV{'LOGNAME'}.'@'.$hostname;
617            $Cemail = '#';
618        }
619    } else {
620        $gecos = 'Unknown Caff User';
621        $email = $ENV{'LOGNAME'}.'@'.$hostname;
622        @keys = qw{0123456789ABCDEF 89ABCDEF76543210};
623        ($Cgecos,$Cemail,$Ckeys) = ('#','#','#');
624    };
625
626    my $template = <<EOT;
627# .caffrc -- vim:ft=perl:
628# This file is in perl(1) format - see caff(1) for details.
629
630$Cgecos\$CONFIG{'owner'} = '$gecos';
631$Cemail\$CONFIG{'email'} = '$email';
632#\$CONFIG{'reply-to'} = 'foo\@bla.org';
633
634# You can get your long keyid from
635#   $CONFIG{gpg} --keyid-format long --list-key <yourkeyid|name|emailaddress..>
636#
637# If you have a v4 key, it will simply be the last 16 digits of
638# your fingerprint.
639#
640# Example:
641#   \$CONFIG{'keyid'} = [ qw{FEDCBA9876543210} ];
642#  or, if you have more than one key:
643#   \$CONFIG{'keyid'} = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
644$Ckeys\$CONFIG{'keyid'} = [ qw{@keys} ];
645
646# Select this/these keys to sign with
647#\$CONFIG{'local-user'} = [ qw{@keys} ];
648
649# Additionally encrypt messages for these keyids
650#\$CONFIG{'also-encrypt-to'} = [ qw{@keys} ];
651
652# Mail template to use for the encrypted part
653#\$CONFIG{'mail-template'} = << 'EOM';
654EOT
655
656    $template .= "#$_" foreach <DATA>;
657    $template .= "#EOM\n";
658    return $template;
659}
660
661my @GNUPGOPTS;
662sub load_config() {
663    my $config = $ENV{'HOME'} . '/.caffrc';
664    unless (-f $config) {
665        notice "No configfile $config present, I will use this template:";
666        my $template = generate_config();
667        print $template, "\n";
668        notice "Please edit $config and run caff again.";
669        open F, '>', $config or myerror(1, "$config: $!");
670        print F $template;
671        close F;
672        exit(1);
673    }
674    unless (scalar eval `cat $config`) {
675        myerror(1, "Couldn't parse $config: $@") if $@;
676    };
677
678    myerror(1, "$0: $_ is not defined in $config") for grep {!defined $CONFIG{$_}} qw/owner email keyid/;
679    myerror(1, "$0: keyid is not an array ref in $config") unless ref $CONFIG{'keyid'} eq 'ARRAY';
680    myerror(1, "$0: key $_ is not specified as a long (16 digit) keyid or fingerprint in $config") for
681        grep !/^((?:0x)?\p{AHex}{16}|\p{AHex}{40}|(?:\p{AHex}{4} ){5}(?: \p{AHex}{4}){5})$/, @{$CONFIG{'keyid'}};
682
683    $CONFIG{'caffhome'} //= $ENV{'HOME'}.'/.caff';
684    $KEYSBASE  = $CONFIG{'caffhome'}.'/keys';
685    $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
686    foreach ($CONFIG{'caffhome'}, $KEYSBASE, $GNUPGHOME) {
687        next if -d $_;
688        debug("Creating $_");
689        mkdir $_, 0700 or myerror(1, "Cannot mkdir $_: $!");
690    }
691
692    @{$CONFIG{'keyid'}} = map { s/^0x//; uc (substr y/ //dr, -16) } @{$CONFIG{'keyid'}}; # must be a list of long keyids
693    $CONFIG{'export-sig-age'} //= 24*60*60;
694    $CONFIG{'gpg'} //= $ENV{GNUPGBIN} // 'gpg';
695
696    $CONFIG{'secret-keyring'} //= ($ENV{'GNUPGHOME'} // "$ENV{'HOME'}/.gnupg") . '/secring.gpg';
697    $CONFIG{'no-download'} //= 0;
698    $CONFIG{'no-sign'} //= 0;
699    $CONFIG{'key-files'} //= [];
700    $CONFIG{'mailer-send'} //= [];
701    myerror(1, "$0: mailer-send is not an array ref in $config") unless ref $CONFIG{'mailer-send'} eq 'ARRAY';
702    $CONFIG{'mail-subject'} //= "Your signed PGP key 0x%k";
703    $CONFIG{'mail-template'} //= do { local $/; <DATA> };
704    $CONFIG{'also-encrypt-to'} = [ $CONFIG{'also-encrypt-to'} ]
705        if defined $CONFIG{'also-encrypt-to'} and !ref $CONFIG{'also-encrypt-to'};
706    if (defined $LOCALE) {
707        $CONFIG{$_} = $LOCALE->decode($CONFIG{$_}) for qw/owner mail-template mail-subject/;
708        $CONFIG{$_} = email_to_ascii($LOCALE->decode($CONFIG{$_}))
709            for grep {defined $CONFIG{$_}} qw/email bcc reply-to/;
710    }
711    $CONFIG{'gpg-sign-type'} //= '';
712    myerror(1, "$0: $CONFIG{'gpg-sign-type'} is an invalid signature type")
713        unless $CONFIG{'gpg-sign-type'} =~ /^(?:l|nr|t)*$/;
714    $CONFIG{'also-lsign-in-gnupghome'} //= 'no';
715    $CONFIG{'also-lsign-in-gnupghome'} = 'no' if $CONFIG{'no-sign'};
716    myerror(1, "$0: invalid value for 'also-lsign-in-gnupghome': $CONFIG{'also-lsign-in-gnupghome'}")
717        unless grep { $_ eq $CONFIG{'also-lsign-in-gnupghome'} } qw/auto ask no/;
718    $CONFIG{'show-photos'} //= 0;
719
720    $CONFIG{colors} //= {
721        error => 'bold bright_red',
722        warn => 'bright_red',
723        notice => 'bold',
724        info => '',
725        success => 'green',
726        fail => 'yellow'
727    };
728
729    # Import some options from ~/.gnupghome/gpg.conf.  We don't symlink
730    # the whole file because the user could have set an option in
731    # ~/.gnupg/gpg.conf which would break caff.
732    my $gpgconf = ($ENV{'GNUPGHOME'} // "$ENV{'HOME'}/.gnupg") . '/gpg.conf';
733    if ( ! -f "$GNUPGHOME/gpg.conf" and -f $gpgconf and open my $fh, '<', $gpgconf) {
734        # the list of options to import from ~/.gnupg/gpg.conf; only
735        # safe (and useful) options for caff should be listed here
736        my @gnupgopts_n = qw/
737            disable-ccid
738            ask-cert-level no-ask-cert-level
739            use-agent no-use-agent
740            no-random-seed-file
741            no-greeting
742            expert no-expert
743            gnupg no-pgp2 pgp6 no-pgp6 pgp7 no-pgp7 pgp8 no-pgp8
744            rfc2440 rfc4880 openpgp
745            default-comment no-comments
746            emit-version no-emit-version no-version
747            allow-weak-digest-algos
748            ask-cert-expire no-ask-cert-expire
749            utf8-strings no-utf8-strings
750            only-sign-text-ids
751        /;
752        my @gnupgopts_i = qw/
753            default-cert-level
754            limit-card-insert-tries
755        /;
756        my @gnupgopts_s = qw/
757            photo-viewer
758            exec-path
759            pcsc-driver
760            reader-port
761            display-charset charset
762            keyserver keyserver-options
763            gpg-agent-info
764            personal-cipher-preferences personal-digest-preferences
765            comment
766            cert-notation set-notation
767            cert-policy-url set-policy-url
768            cipher-algo cert-digest-algo
769            pinentry-mode
770            weak-digest
771            default-cert-expire
772            disable-cipher-algo disable-pubkey-algo
773            agent-program dirmngr-program
774        /;
775
776        push @GNUPGOPTS, '--no-options';
777        notice('Importing GnuPG options from '.($ENV{'GNUPGHOME'} // '~/.gnupg').'/gpg.conf:');
778        while (<$fh>) {
779            s/(?:\r\n|\r|\n)\z// or next;
780            if (/\A\s*([0-9a-zA-Z\-]+)\z/ and grep { $_ eq $1 } @gnupgopts_n) {
781                push @GNUPGOPTS, "--$1";
782            } elsif (/\A\s*([0-9a-zA-Z\-]+)\s+(\d+)\z/ and grep { $_ eq $1 } @gnupgopts_i) {
783                push @GNUPGOPTS, "--$1=$2";
784            } elsif (/\A\s*([0-9a-zA-Z\-]+)\s+(\P{Control}+)\z/ and grep { $_ eq $1 } @gnupgopts_s) {
785                push @GNUPGOPTS, "--$1=$2";
786            } else {
787                next;
788            }
789            notice("    $_");
790        }
791        close $fh;
792    }
793
794    # deprecated options, will be removed in a future release
795    mywarn("Deprecated option \$CONFIG{'$_'} = '$CONFIG{$_}'") foreach
796        grep {defined $CONFIG{$_}} qw{gpg-sign gpg-delsig keyserver};
797}
798
799# Create a new GnuPG::Interface object with common options
800sub mkGnuPG(%) {
801    my %h = @_;
802    my $gpg = GnuPG::Interface::->new();
803    $gpg->call( $CONFIG{'gpg'} );
804
805    $h{meta_interactive} //= 0;
806    $h{always_trust} //= 1;
807    $h{extra_args} //= [];
808
809    unshift @{$h{extra_args}}, '--no-auto-check-trustdb';
810    unshift @{$h{extra_args}}, '--fixed-list-mode' if GnuPG_version('2.0.0') <  0;
811    unshift @{$h{extra_args}}, '--no-autostart'    if GnuPG_version('2.1.0') >= 0; # never autostart
812    unshift @{$h{extra_args}}, @GNUPGOPTS if @GNUPGOPTS and defined $h{homedir};
813
814    $gpg->options->hash_init(%h);
815    debug(join (' ', $gpg->call(), $gpg->options->get_args(), "..."));
816    return $gpg;
817}
818
819
820# Create a GnuPG::Handles object.  This function takes a hash where keys
821# are handle names, and values are either IO::Handle objects, in which
822# case the existing handle is used, or undefined, in which case a new
823# IO::Handle is created.
824sub mkGnuPG_fds(%) {
825    my %fd = @_;
826    my @direct;
827
828    foreach (keys %fd) {
829        push @direct, $_ if defined $fd{$_} and $fd{$_} !~ /^[<>]&/;
830        $fd{$_} //= IO::Handle::->new();
831    }
832
833    # Redirect the STDIN and STDOUT to /dev/null unless explicitly
834    # redirected.  Also redirect logger to /dev/null in non-debug mode,
835    # but NEVER redirect STDERR!
836    $fd{stdin}  = "<&=$NULL" unless exists $fd{stdin};
837    $fd{stdout} = ">&=$NULL" unless exists $fd{stdout};
838    $fd{logger} = ">&=$NULL" unless exists $fd{logger} or $PARAMS->{debug};
839
840    my $handles = GnuPG::Handles::->new(%fd);
841    $handles->options($_)->{direct} = 1 foreach @direct;
842    debug(join (', ', map {"$_: " . ($handles->options($_)->{direct} ? $fd{$_}->fileno : $fd{$_})} keys %fd));
843
844    return $handles;
845}
846
847sub done_gpg($;$) {
848    my ($pid, $handles) = @_;
849    waitpid $pid, 0;
850    mywarn("$CONFIG{gpg} exited with value ".($? >> 8)) if $? > 0;
851    return unless defined $handles;
852    foreach (GnuPG::Handles::HANDLES) {
853        next unless defined $handles->{$_} and $handles->{$_} !~ /^[<>]&/;
854        $handles->{$_}->close if $handles->{$_}->opened;
855    }
856}
857
858
859# Send some data on GnuPG handles, and retrieve output from all handles
860# at once using select(2) syscalls.  Stop when some output matches a
861# given regex, or when there nothing more to read or write.  A newline
862# '\n' character is automatically appended to the text to be send to the
863# 'command' handle; the prefix "[GNUPG:] " to the 'status' handle is
864# added as well.
865sub readwrite_gpg($%) {
866    my $handles = shift;
867    my %opts = @_;
868
869    # ignore direct and dup handles
870    my @infhs  = grep {defined $opts{$_}      and !$handles->options($_)->{direct} and $handles->{$_} !~ /^[<>]&/} qw/stdin passphrase command/;
871    my @outfhs = grep {defined $handles->{$_} and !$handles->options($_)->{direct} and $handles->{$_} !~ /^[<>]&/} qw/stdout stderr status logger/;
872    my %fh = map { $handles->{$_} => $_ } (@infhs, @outfhs);
873
874    my %offset = map {$_ => 0}  @infhs;
875    my %output = map {$_ => ''} @outfhs;
876
877    if (defined $opts{command}) {
878        # automatically send the command
879        chomp $opts{command};
880        $opts{command} .= "\n";
881    }
882    $opts{status} = qr/^\[GNUPG:\] $opts{status}$/m if defined $opts{status};
883
884    $handles->{$_}->blocking(0) foreach (@infhs, @outfhs);
885    my $sin  = IO::Select::->new(map {$handles->{$_}} @infhs);
886    my $sout = IO::Select::->new(map {$handles->{$_}} @outfhs);
887
888    trace("entering readwrite_gpg.");
889    trace("doing stuff until one of: ". join(', ', map {"$_ =~ $opts{$_}"} grep {defined $opts{$_}} @outfhs))
890        if grep {defined $opts{$_}} @outfhs;
891
892    my $readwrote_stuff_this_time = 0;
893    my $do_not_wait_on_select = 0;
894    while ($sin->count() + $sout->count() > 0) {
895        if (!$sin->count() and grep {defined $opts{$_} and $output{$_} =~ $opts{$_}} @outfhs) {
896            if ($readwrote_stuff_this_time) {
897                trace("read/write some more.");
898                $do_not_wait_on_select = 1;
899            } else {
900                trace("that's it in our while loop.");
901                last;
902            }
903        };
904
905        trace("select waiting for ".($sin->count()+$sout->count())." fds.");
906        my ($readyr, $readyw, undef) = IO::Select::select($sout, $sin, undef, $do_not_wait_on_select ? 0 : 1);
907        trace("ready: write: ". join (',', map {$fh{$_}} @{$readyw // []}).
908                    "; read: ". join (',', map {$fh{$_}} @{$readyr // []}));
909        $readwrote_stuff_this_time = 0;
910
911        for my $fd (@{$readyw // []}) {
912            $readwrote_stuff_this_time = 1;
913            my $fh = $fh{$fd};
914            if ($offset{$fh} != length $opts{$fh}) {
915                trace ("writing to '$fh'". ($offset{$fh} ? "" : ": ".(split /\n/, $opts{$fh}, 2)[0]));
916                my $written = $fd->syswrite($opts{$fh}, length($opts{$fh}) - $offset{$fh}, $offset{$fh});
917                $offset{$fh} += $written;
918            }
919            if ($offset{$fh} == length $opts{$fh}) {
920                trace "done writing to '$fh'.";
921                $sin->remove($fd);
922                $fd->close && trace "closed '$fh'." if $opts{autoclose};
923            }
924        }
925        for my $fd (@{$readyr // []}) {
926            $readwrote_stuff_this_time = 1;
927            my $fh = $fh{$fd};
928            if ($fd->eof) {
929                trace "done reading from '$fh'.";
930                $sout->remove($fd);
931                next;
932            }
933            trace "reading from '$fh'.";
934            $output{$fh} .= do { local $/; <$fd> };
935            trace2 "$fh is now:\n$output{$fh}\n================";
936        }
937    }
938    trace("readwrite_gpg done.");
939    return %output;
940}
941
942
943sub ask($$;$$) {
944    my ($question, $default, $forceyes, $forceno) = @_;
945
946    my $fd = fileno(TTY) // die;
947    my $termios = POSIX::Termios::->new();
948    $termios->getattr($fd) // die "getattr: $!";
949    my $echo = POSIX::ECHO | POSIX::ECHOK | POSIX::ICANON;
950    my $c_iflag = $termios->getiflag();
951    my $c_lflag = $termios->getlflag();
952    unless ($c_iflag & POSIX::ICRNL and ($c_lflag & $echo) == $echo) {
953        debug("Sanitizing TTY");
954        $termios->setiflag( $c_iflag | POSIX::ICRNL );
955        $termios->setlflag( $c_lflag | $echo );
956        $termios->setattr($fd, POSIX::TCSANOW) // warn "setattr: $!";
957    }
958
959    my $answer;
960    my $yn = $default ? '[Y/n]' : '[y/N]';
961    while (1) {
962        print $question,' ',$yn, ' ';
963        if ($forceyes && $forceno) {
964            print "$default (from config/command line)\n";
965            return $default;
966        };
967        if ($forceyes) {
968            print "YES (from config/command line)\n";
969            return 1;
970        };
971        if ($forceno) {
972            print "NO (from config/command line)\n";
973            return 0;
974        };
975
976        $answer = <TTY>;
977        chomp $answer;
978        last if ((length $answer == 0) || ($answer =~ m/^[yYnN]$/) );
979        print "What about $yn is so hard to understand?\nAnswer with either 'n' or 'y' or just press enter for the default.\n";
980        sleep 1;
981    };
982    my $result = $default;
983    $result = 1 if $answer =~ /y/i;
984    $result = 0 if $answer =~ /n/i;
985    return $result;
986}
987
988
989
990
991
992my $KEYEDIT_PROMPT = qr/GET_LINE keyedit\.prompt/;
993my $KEYEDIT_DELUID_PROMPT = qr/GET_BOOL keyedit\.remove\.uid\.okay/;
994my $KEYEDIT_DELSIG_PROMPT = qr/GET_BOOL keyedit\.delsig\.(?:unknown|invalid|valid)/; # we won't delete selfsigs
995my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = qr/$KEYEDIT_PROMPT|$KEYEDIT_DELSIG_PROMPT/;
996my $KEYEDIT_DELSUBKEY_PROMPT = qr/GET_BOOL keyedit\.remove\.subkey\.okay/;
997my $KEYEDIT_SIGNUID_CLASS_PROMPT = qr/GET_LINE sign_uid\.class/;
998my $KEYEDIT_SIGNUID_PROMPT = qr/GET_BOOL sign_uid\.okay/;
999
1000
1001sub version($) {
1002    my ($fd) = @_;
1003    print $fd "$0 $VERSION - (c) 2004, 2005, 2006 Peter Palfrader et al.\n";
1004}
1005
1006sub usage($$) {
1007    my ($fd, $exitcode) = @_;
1008    version($fd);
1009    print $fd "Usage: $0 [-eERS] [-m <yes|ask-yes|ask-no|no>] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
1010    print $fd "Consult the manual page for more information.\n";
1011    exit $exitcode;
1012}
1013
1014######
1015# export keys @$keyids from $gnupghome.  In list context, fork and
1016# return the pid and the file descriptor of its standard output;
1017# otherwise, wait until the export is done and return the ASCII key.
1018#
1019# /!\ Failure to export a key will not be detected, unless *all* keys
1020#       couldn't be exported.  Therefore for safe export/import, you need
1021#       to inspect '$asciikey' or the status FD on the import side.
1022######
1023sub export_keys($$@) {
1024    my ($gnupghome, $keyids, @export_options) = @_;
1025    myerror(1, "Nothing to export") unless defined $keyids and @$keyids;
1026    my @extra_args = ('--export-options', join (',', @export_options)) if @export_options;
1027
1028    # don't armor when piping since it's faster
1029    my $gpg = mkGnuPG( homedir => $gnupghome, armor => (wantarray ? 0 : 1), extra_args => \@extra_args );
1030    my $handles = mkGnuPG_fds( stdout => undef );
1031    my $pid = $gpg->export_keys( handles => $handles, command_args => $keyids );
1032
1033    if (wantarray) {
1034        return ($pid, $handles->{stdout});
1035    } else {
1036        my $asciikey = do { local $/; readline $handles->{stdout} };
1037        done_gpg($pid, $handles);
1038        return $asciikey;
1039    }
1040}
1041
1042
1043######
1044# Create an email to $address.  If $can_encrypt is true then the mail
1045# will be PGP/MIME encrypted to $longkeyid.
1046#
1047# $longkeyid, $uid, and @attached will be used in the email and the template.
1048######
1049# create_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
1050sub create_mail($$$@) {
1051    my ($address, $can_encrypt, $key_id, @keys) = @_;
1052
1053    my $template = Text::Template::->new(TYPE => 'STRING', SOURCE => $CONFIG{'mail-template'});
1054    myerror(1, "Cannot create template: $Text::Template::ERROR") unless defined $template;
1055
1056    my $message = $template->fill_in(HASH => { key => $key_id,
1057                           uids => [ map {$_->{'text'}} @keys ],
1058                           owner => $CONFIG{'owner'}});
1059    myerror(1, "Cannot fill in template: $Text::Template::ERROR") unless defined $message;
1060
1061    my $message_entity = MIME::Entity->build(
1062        Type        => "text/plain",
1063        Charset     => "utf-8",
1064        Disposition => 'inline',
1065        Data        => Encode::encode_utf8($message));
1066
1067    my @key_entities;
1068    for my $key (@keys) {
1069        $message_entity->attach(
1070            Type        => "application/pgp-keys",
1071            Disposition => 'attachment',
1072            Encoding    => "7bit",
1073            Description => "PGP Key 0x$key_id, uid ".Encode::encode_utf8($key->{text})." ($key->{serial}), signed by 0x$CONFIG{keyid}[0]",
1074            Data        => $key->{key},
1075            Filename    => "0x$key_id.$key->{serial}.signed-by-0x$CONFIG{keyid}[0].asc");
1076    };
1077
1078    if ($can_encrypt) {
1079        my $gpg = mkGnuPG( homedir => $GNUPGHOME, armor => 1, textmode => 1 );
1080        $gpg->options->push_recipients($key_id);
1081        $gpg->options->push_recipients(@{$CONFIG{'also-encrypt-to'}}) if defined $CONFIG{'also-encrypt-to'};
1082        my $handles = mkGnuPG_fds( stdin => undef, stdout => undef, status => undef );
1083        my $pid = $gpg->encrypt(handles => $handles);
1084        my %output = readwrite_gpg($handles, stdin => $message_entity->stringify(), autoclose => 1);
1085        done_gpg($pid, $handles);
1086        my ($message, $status) = @output{qw/stdout status/};
1087
1088        if ($message eq '') {
1089            if ($status =~ /^\[GNUPG:\] INV_RECP ([0-9]+) ([0-9A-F]+)$/m and defined $CONFIG{'also-encrypt-to'}) {
1090                my $reason = $1;
1091                my $keyid = $2;
1092                if (grep { $_ eq $keyid } @{$CONFIG{'also-encrypt-to'}}) {
1093                    mywarn "Could not encrypt to $keyid, specified in CONFIG{'also-encrypt-to'}";
1094                    mywarn "Try to update the key using gpg --homedir=$GNUPGHOME --import <exported key>";
1095                    return;
1096                };
1097            };
1098            mywarn "No data from gpg for encrypting mail; status output was:\n$status";
1099            return;
1100        };
1101
1102        $message_entity = MIME::Entity->build(
1103            Type        => 'multipart/encrypted; protocol="application/pgp-encrypted"',
1104            Encoding    => '7bit' );
1105
1106        $message_entity->attach(
1107            Type        => "application/pgp-encrypted",
1108            Filename    => "signedkey.msg",
1109            Disposition => 'attachment',
1110            Encoding    => "7bit",
1111            Data        => "Version: 1\n" );
1112
1113        $message_entity->attach(
1114            Type        => "application/octet-stream",
1115            Filename    => 'msg.asc',
1116            Disposition => 'inline',
1117            Encoding    => "7bit",
1118            Data        => $message );
1119    };
1120
1121    my $from = Encode::encode('MIME-Q', $CONFIG{owner})." <$CONFIG{email}>";
1122    $message_entity->head->add("From", $from);
1123    $message_entity->head->add("Date", strfCtime("%a, %e %b %Y %H:%M:%S %z", localtime));
1124    $message_entity->head->add("Subject", Encode::encode('MIME-Q', $CONFIG{'mail-subject'} =~ s/%k/$key_id/gr));
1125    $message_entity->head->add("To", $address);
1126    $message_entity->head->add("Sender", $from);
1127    $message_entity->head->add("Reply-To", $CONFIG{'reply-to'}) if defined $CONFIG{'reply-to'};
1128    $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
1129    $message_entity->head->add("User-Agent", $USER_AGENT);
1130    return $message_entity;
1131}
1132
1133######
1134# send a mail message (MIME::Entity)
1135######
1136my $warned_about_broken_mailer_send = 0;
1137sub send_message($) {
1138    my ($message_entity) = @_;
1139
1140    if ((scalar @{$CONFIG{'mailer-send'}} > 0) && !$warned_about_broken_mailer_send) {
1141        mywarn("You have set arguments to pass to Mail::Mailer.  Better fix your MTA.  (Also, Mail::Mailer's error reporting is non existent, so it won't tell you when it doesn't work.)");
1142        $warned_about_broken_mailer_send = 1;
1143    };
1144    $message_entity->send(@{$CONFIG{'mailer-send'}});
1145}
1146
1147######
1148# clean up a UID so that it can be used on the FS.
1149######
1150sub sanitize_uid($) {
1151    my ($uid) = @_;
1152
1153    my $good_uid = $uid;
1154    $good_uid =~ tr#/:\\#_#;
1155    trace2("[sanitize_uid] changed UID from $uid to $good_uid.\n") if $good_uid ne $uid;
1156    return $good_uid;
1157}
1158
1159# Delete all non self-sigs that are not made by one of the @$keyids, and
1160# return the date of the most recent signature and a hash reference
1161# {$signer => $level} of the keys in @$keyids that have an exportable
1162# signature on that $uid.  If $keep_lsigs_only, our exportable
1163# signatures are removed as well.
1164sub delete_signatures($$$$;$) {
1165    my ($handles, $longkeyid, $uid, $keyids, $keep_lsigs_only) = @_;
1166
1167    readwrite_gpg($handles, command => "uid 0",    status => $KEYEDIT_PROMPT); # unmark all uids from delsig
1168    readwrite_gpg($handles, command => "uid $uid", status => $KEYEDIT_PROMPT); # mark $uid for delsig
1169
1170    my $last_signed_on = 0;
1171    my %xsigners;
1172
1173    my %output = readwrite_gpg($handles, command => "delsig", status => $KEYEDIT_DELSIG_PROMPT);
1174
1175    while($output{status} =~ /$KEYEDIT_DELSIG_PROMPT/m) {
1176        # sig:?::17:EA2199412477CAF8:1058095214:::::13x
1177        my @sigline = grep /^sig:/, (split /\n/, $output{stdout});
1178        my $answer = "no";
1179        if (!@sigline) {
1180            debug("[sigremoval] no sig line here, only got:\n".$output{stdout});
1181        }
1182        else { # only if we found a sig here - we never remove revocation packets for instance
1183            my $sig = pop @sigline;
1184            $sig =~ /^sig:(?:[^:]*:){3}([0-9A-F]{16}):(\d+):(?:[^:]*:){4}(1[0-3]|30)([lx])(?::.*)?$/ or
1185                mywarn("I hit a bug, please report: Couldn't parse sigline $sig");
1186            debug("[sigremoval] doing sigline $sig");
1187            if ($1 eq $longkeyid) {
1188                debug("[sigremoval] selfsig ($1)");
1189                $answer = "no";
1190            } elsif (grep { $1 eq $_ } @$keyids and $3 != 30) {
1191                debug("[sigremoval] signed by us ($1)");
1192                $answer = ($keep_lsigs_only and $4 eq 'x') ? "yes" : "no";
1193                $last_signed_on = $2 if $last_signed_on < $2;
1194                $xsigners{$1} = $3-10 if $4 eq 'x';
1195            } else {
1196                debug("[sigremoval] not interested in that sig ($1)");
1197                $answer = "yes";
1198            };
1199            mywarn("I hit a bug, please report: Found the following ".($#sigline+2)." siglines in that part of the dialog:\n".$output{stdout}) if @sigline;
1200        }
1201        %output = readwrite_gpg($handles, command => $answer, status => $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT);
1202    }
1203
1204    return ($last_signed_on, \%xsigners);
1205}
1206
1207##
1208# Check the local user keys.
1209#
1210# This function checks if the keyids defined through the --local-user
1211# command line option or set in ~/.caffrc are valid and known to be one of the
1212# keyids listed in ~/.caffrc.
1213#
1214# @return an array containing the local user keys\n
1215#         (undef) if no valid key has been found
1216#
1217sub get_local_user_keys() {
1218    # No user-defined key id has been specified by the user, no need for
1219    # further checks
1220    return @{$CONFIG{'keyid'}} unless $CONFIG{'local-user'};
1221
1222    # Parse the list of keys
1223    my @key_list = ref $CONFIG{'local-user'} ?  @{$CONFIG{'local-user'}} : split /\s*,\s*/, $CONFIG{'local-user'};
1224    my @local_user;
1225
1226    # Check every key defined by the user...
1227    for my $user_key (@key_list) {
1228        unless ($user_key =~ m/^((?:0x)?\p{AHex}{8}|(?:0x)?\p{AHex}{16}|\p{AHex}{40}|(?:\p{AHex}{4} ){5}(?: \p{AHex}{4}){5})$/) {
1229            mywarn "Local-user $user_key is not a valid keyid";
1230            next;
1231        }
1232
1233        $user_key =~ s/^0x//;
1234        $user_key =~ y/ //d;
1235        if (my @matching_keyids = grep {$user_key =~ /\Q$_\E$/i or /\Q$user_key\E$/i} @{$CONFIG{'keyid'}}) {
1236            push @local_user, @matching_keyids; # @{$CONFIG{'keyid'}} is always a list of long keyids
1237        } else {
1238            mywarn "Local-user $user_key is not defined as one of your keyid in ~/.caffrc (it will not be used)";
1239        }
1240    }
1241
1242    # If no local-user key are valid, there is no need to go further
1243    myerror(1, "None of the local-user keys seem to be known as a keyid listed in ~/.caffrc") unless @local_user;
1244    return @local_user;
1245}
1246
1247##
1248# Import keys from a gnupghome to another.
1249#
1250# @param keyids         keyids of the OpenPGP keys to import
1251# @param src_gnupghome  gnupghome directory where to export the key from
1252# @param dst_gnupghome  gnupghome directory where to import the key into
1253# @param die_on_error   whether to die if some of the keyids couldn't be imported
1254# @param import_options a list of import-options, see gpg(1)
1255#
1256# @ return a hash reference mapping each key ID to the list of matching
1257#   imported key fingerprint.
1258#
1259sub import_keys_from_gnupghome($$$$@) {
1260    my ($keyids, $src_gpghome, $dst_gpghome, $die_on_error, @import_options) = @_;
1261    my $src = $src_gpghome // "your normal GnuPGHOME";
1262    my $dst = $dst_gpghome // "your normal GnuPGHOME";
1263
1264    my @extra_args;
1265    push @import_options, 'import-local-sigs' if $CONFIG{'gpg-sign-type'} =~ /l/ and !grep /import-local-sigs$/, @import_options;
1266    push @import_options, 'keep-ownertrust' unless defined $dst_gpghome or GnuPG_version('2.1.0') >= 0; # don't modify our own trustdb
1267    push @extra_args, '--min-cert-level=1' if grep { $_ eq 'import-clean' } @import_options;
1268    push @extra_args, '--import-options', join (',', @import_options) if @import_options;
1269
1270    # export the (non-armored) keys to $pipe
1271    debug("Exporting key(s) ".(join ',', @$keyids)." from $src to $dst");
1272    my @export_options = ('export-local-sigs') if grep {$_ eq 'import-local-sigs'} @import_options;
1273    my ($ePid, $pipe) = export_keys($src_gpghome, $keyids, @export_options);
1274
1275    my $gpg = mkGnuPG( homedir => $dst_gpghome, quiet => 1, extra_args => \@extra_args );
1276    my $handles = mkGnuPG_fds( stdin  => $pipe, status => undef ); # import keys from $pipe
1277    my $iPid = $gpg->import_keys( handles => $handles );
1278
1279    my $status = import_loop($handles->{status}, defined $src_gpghome ? 0 : 1, $keyids);
1280    done_gpg($iPid, $handles);  # import done
1281    waitpid $ePid => 0;         # export done
1282
1283    my @failed = grep { !@{$status->{$_}} } keys %$status;
1284    if (@failed) {
1285        my $msg = "Couldn't import key(s) ".(join ',', @failed)." from $src";
1286        $die_on_error ? myerror(1, $msg) : info($msg, 0);
1287    }
1288    return $status;
1289}
1290
1291##
1292# Import loop.
1293#
1294# @param fh                 the status file handle from GnuPG::Interface
1295# @param verbose            whether to list the status of each key as it
1296#                           is being imported.
1297# @param keyids             an array of keyids to be imported
1298# @param ignore_unexpected  whether not to print a warning upon receiving
1299#                           an unexpected key
1300#
1301# @ return a hash reference mapping each key ID to the list of matching
1302#   imported key fingerprint.
1303#
1304sub import_loop($$$;$) {
1305    my ($fh, $verbose, $keyids, $ignore_unexpected) = @_;
1306
1307    # [GNUPG:] IMPORT_OK 0 5B00C96D5D54AEE1206BAF84DE7AAF6E94C09C7F
1308    # [GNUPG:] NODATA 1
1309    # [GNUPG:] NODATA 1
1310    # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
1311    my %status = map { $_ => [] } @$keyids;
1312    while (<$fh>) {
1313        # inspect the $status FD as data gets out
1314        if (/^\[GNUPG:\] IMPORT_OK (\d+) ([0-9A-F]{40})$/) {
1315            my ($r, $fpr) = ($1, $2);
1316            my @matching_keyids = grep { $fpr =~ /\Q$_\E$/ } @$keyids;
1317            unless (@matching_keyids) {
1318                unless ($ignore_unexpected) {
1319                    mywarn("Imported unexpected key $fpr.  Are you trying to work on a subkey?");
1320                } elsif ($verbose) {
1321                    info( "Key $fpr ". ($r == 0 ? 'not changed' : 'imported'), ($r == 0 ? undef : 1) );
1322                }
1323                next;
1324            }
1325            debug( "Imported $fpr for ".join(',', @matching_keyids));
1326            info( "Key " .join(',', @matching_keyids).' '. ($r == 0 ? 'not changed' : 'imported'), ($r == 0 ? undef : 1) ) if $verbose;
1327            push @{$status{$_} //= []}, $fpr foreach @matching_keyids;
1328        }
1329        elsif (/^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{32})$/) {
1330            mywarn("Imported v3 key $1.  Version 3 keys are obsolete, should not be used, and are not and will not be properly supported.");
1331        }
1332        elsif (!/^\[GNUPG:\]\ (?:NODATA\ \d
1333                               | IMPORT_RES\ .+
1334                               | IMPORTED\ .+
1335                               | KEYEXPIRED\ \d+
1336                               | SIGEXPIRED\ (?:\ deprecated-use-keyexpired-instead)?
1337                               | KEY_CONSIDERED\ [0-9A-F]{40}\ \d+
1338                               | FAILURE\ recv-keys\ \d+
1339                               )$/x) {
1340            mywarn("Got unknown reply from gpg: ".$_);
1341        }
1342    }
1343    return \%status;
1344}
1345
1346##
1347# Import keys to be signed into caff gnupghome directory.
1348#
1349# This function imports the keys the user wants to sign into the caff gnupghome
1350# directory. We looks for the keys in the the user gnupghome directory first,
1351# and in the key files specified by the user if not all of the keys have been
1352# found.
1353#
1354sub import_keys_to_sign($) {
1355    my $keyids = shift;
1356    return unless $CONFIG{'keys-from-gnupg'} or @{$CONFIG{'key-files'}} or !$CONFIG{'no-download'};
1357
1358    # map each keyid to a list of matching fingerprints; there is a
1359    # collision if a keyid is mapped to multiple fingerprints, but we'll
1360    # detect that later in the code
1361    my $status = { map { $_ => [] } @$keyids };
1362
1363    if ($CONFIG{'keys-from-gnupg'}) {
1364        notice("Importing keys from your normal GnuPGHOME (".($ENV{'GNUPGHOME'} // "~/.gnupg").")");
1365        merge_import_status( $status, import_keys_from_gnupghome($keyids, undef, $GNUPGHOME, 0) );
1366    }
1367
1368    foreach my $keyfile (@{$CONFIG{'key-files'}}) {
1369        notice("Importing key file $keyfile");
1370
1371        my $gpg = mkGnuPG( homedir => $GNUPGHOME, quiet => 1 );
1372        $gpg->options->push_extra_args(qw/--import-options import-local-sigs/) if $CONFIG{'gpg-sign-type'} =~ /l/;
1373        my $handles = mkGnuPG_fds( status => undef );
1374        my $pid = $gpg->import_keys( handles => $handles, command_args => $keyfile );
1375
1376        merge_import_status( $status, import_loop($handles->{status}, 1, $keyids, 1) );
1377        done_gpg($pid, $handles);
1378    }
1379
1380    # Receive keys from keyserver
1381    unless ($CONFIG{'no-download'}) {
1382        notice("Fetching keys from a keyserver (this may take a while)...");
1383        my @args = (extra_args => ['--keyserver='.$CONFIG{'keyserver'}]) if defined $CONFIG{'keyserver'};
1384        my $gpg = mkGnuPG( homedir => $GNUPGHOME, @args );
1385        # logger: requesting key ... from hkp
1386        # stdout: gpgkeys: key ... not found on keyserver
1387        my $handles = mkGnuPG_fds( status => undef );
1388        my $pid = $gpg->recv_keys(handles => $handles, command_args => $keyids);
1389
1390        my $s = import_loop($handles->{status}, 1, $keyids);
1391        merge_import_status($status, $s);
1392        done_gpg($pid, $handles);
1393
1394        my @failed = grep { !@{$s->{$_}} } keys %$s;
1395        info("Couldn't import key(s) ".(join ',', @failed)." from the keyserver", 0) if @failed;
1396    }
1397
1398    my @failed = grep { !@{$status->{$_}} } keys %$status;
1399    if (@failed) {
1400        exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
1401        mywarn("Assuming ". join(' ', @failed).' '.($#failed > 0 ? 'are' : 'is a')." fine keyid".($#failed > 0 ? 's' : ''));
1402    }
1403}
1404sub merge_import_status($$) {
1405    foreach my $keyid (keys %{$_[1]}) {
1406        push @{$_[0]->{$keyid} //= []}, @{$_[1]->{$keyid}};
1407    }
1408}
1409
1410##
1411# A non-localized version of POSIX::strftime.
1412#
1413sub strfCtime($@) {
1414    my $lc_time = setlocale(POSIX::LC_TIME);
1415    setlocale(POSIX::LC_TIME, 'C');
1416    my $str = strftime(@_);
1417    setlocale(POSIX::LC_TIME, $lc_time);
1418    return $str;
1419}
1420
1421
1422###################
1423# argument handling
1424###################
1425Getopt::Long::config('bundling');
1426if (!GetOptions (
1427    '-h'                =>  \$PARAMS->{'help'},
1428    '--help'            =>  \$PARAMS->{'help'},
1429    '--version'         =>  \$PARAMS->{'version'},
1430    '-V'                =>  \$PARAMS->{'version'},
1431    '-u=s'              =>  \$PARAMS->{'local-user'},
1432    '--local-user=s'    =>  \$PARAMS->{'local-user'},
1433    '-e'                =>  \$PARAMS->{'export-old'},
1434    '--export-old'      =>  \$PARAMS->{'export-old'},
1435    '-E'                =>  \$PARAMS->{'no-export-old'},
1436    '--no-export-old'   =>  \$PARAMS->{'no-export-old'},
1437    '-m:s'              =>  \$PARAMS->{'mail'},
1438    '--mail:s'          =>  \$PARAMS->{'mail'},
1439    '-M'                =>  \$PARAMS->{'no-mail'},
1440    '--no-mail'         =>  \$PARAMS->{'no-mail'},
1441    '-R'                =>  \$PARAMS->{'no-download'},
1442    '--no-download'     =>  \$PARAMS->{'no-download'},
1443    '-S'                =>  \$PARAMS->{'no-sign'},
1444    '--no-sign'         =>  \$PARAMS->{'no-sign'},
1445    '--key-file=s@'     =>  \$PARAMS->{'key-files'},
1446    '--keys-from-gnupg' =>  \$PARAMS->{'keys-from-gnupg'},
1447    '--debug'           =>  \$PARAMS->{'debug'},
1448    )) {
1449    usage(\*STDERR, 1);
1450}
1451if ($PARAMS->{'help'}) {
1452    usage(\*STDOUT, 0);
1453}
1454if ($PARAMS->{'version'}) {
1455    version(\*STDOUT);
1456    exit(0);
1457}
1458
1459load_config();
1460
1461my $NOW = time;
1462my  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($NOW);
1463my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday);
1464
1465
1466if (-t \*STDIN) {
1467    # we're already talking to a TTY
1468    usage(\*STDERR, 1) unless @ARGV;
1469    *TTY = *STDIN;
1470} else {
1471    my @checksums;
1472    my $goodblock;
1473    my $got_input; # detect xargs, /dev/null, ...
1474    while (<STDIN>) {
1475        unless ($got_input) {
1476            notice("Reading gpgparticipants formatted input on STDIN");
1477            $got_input = 1;
1478        }
1479
1480        if (/^(\S+)\s+Checksum:\s+[_ 0-9A-F]+(?:\s+\[(.)\])?$/i) {
1481            # ensure the checksum is (claimed to be) verified
1482            my ($md, $r) = ($1, $2);
1483            while (!defined $r) {
1484                $_ = <STDIN>;
1485                if (/^\s+[_ 0-9A-F]+\s+\[(.)\]$/i) {
1486                    $r = $1;
1487                }
1488                elsif (!/^(:?\s+[_ 0-9A-F]+)?$/i) {
1489                    myerror(1, "Unexpected input line: $_");
1490                }
1491            }
1492            myerror(1, "$md checksum wasn't marked as verified!") unless lc $r eq 'x';
1493            notice "Found $md checksum (marked as verified, assumed good)";
1494            push @checksums, uc $md;
1495        }
1496        elsif (/^(?:-+|_+)$/) {
1497            $goodblock = 0;
1498        }
1499        elsif (/^(#*)\d*\s+\[(.)\] Fingerprint(?:\(s\)|s)? OK\s+\[(.)\] ID OK\s*$/) {
1500            $goodblock = (!$1 and lc $2 eq 'x' and lc $3 eq 'x') ? 1 : 0;
1501        }
1502        elsif (/^ {5,}Key fingerprint = ([A-F0-9]{32}|(?:[A-F0-9]{2} ){8}(?: [A-F0-9]{2}){8})$/) {
1503            mywarn("Ignoring v3 fingerprint ".($1 =~ y/ //dr).".  v3 keys are obsolete.");
1504        }
1505        elsif (/^ {5,}(?:Key fingerprint = )?([A-F0-9]{40}|(?:[A-F0-9]{4} ){5}(?: [A-F0-9]{4}){5})$/) {
1506            my $fpr = ($1 =~ y/ //dr);
1507            if ($goodblock) {
1508                info("Adding fingerprint $fpr", 1);
1509                push @KEYIDS, $fpr;
1510            } else {
1511                info("Ignoring fingerprint $fpr", 0);
1512            }
1513        }
1514    }
1515
1516    if ($got_input) {
1517        if (!@checksums) {
1518            mywarn "No checksum found!";
1519        } elsif (!grep { my $x = $_; grep { $x eq $_ } qw/SHA256 SHA384 SHA512 SHA224/ } @checksums) {
1520            mywarn "No checksum of the SHA-2 family found!";
1521        }
1522    }
1523
1524    close STDIN;
1525    open TTY, '<', '/dev/tty' or myerror(1,"No TTY.")
1526}
1527my $TERMIOS = do {
1528    my $t = POSIX::Termios::->new();
1529    $t->getattr(fileno(TTY)) // die "getattr: $!";
1530    $t;
1531};
1532
1533for my $hashkey (qw{local-user no-download no-sign no-mail mail keys-from-gnupg}) {
1534    $CONFIG{$hashkey} = $PARAMS->{$hashkey} if defined $PARAMS->{$hashkey};
1535}
1536
1537# If old 'no-mail' parameter, or if the 'mail' parameter is set to 'no'
1538if ( defined $CONFIG{'no-mail'} || (defined $CONFIG{'mail'} && $CONFIG{'mail'} eq 'no') ) {
1539    $CONFIG{'mail'} = 'no';
1540} elsif ( !defined $CONFIG{'mail'} ) {
1541    $CONFIG{'mail'} = 'ask-yes';
1542}
1543$CONFIG{'mail-cant-encrypt'} //= $CONFIG{'mail'};
1544
1545push @{$CONFIG{'key-files'}}, @{$PARAMS->{'key-files'}} if defined $PARAMS->{'key-files'};
1546
1547for my $keyid (map { split /\n/ } @ARGV) { # caff "`cat txt`" is a single argument
1548    if ($keyid =~ /^(\p{AHex}{32}|(?:\p{AHex}{2} ){8}(?: \p{AHex}{2}){8})$/) {
1549        mywarn("Ignoring v3 fingerprint ".($keyid =~ y/ //dr).".  v3 keys are obsolete.");
1550        next;
1551    }
1552    elsif ($keyid !~ /^((?:0x)?\p{AHex}{8}|(?:0x)?\p{AHex}{16}|\p{AHex}{40}|(?:\p{AHex}{4} ){5}(?: \p{AHex}{4}){5})$/) {
1553        print STDERR "$keyid is not a keyid.\n";
1554        usage(\*STDERR, 1);
1555    }
1556
1557    $keyid =~ s/^0x//;
1558    $keyid =~ y/ //d; # gpg --fingerprint includes spaces
1559    push @KEYIDS, uc($keyid);
1560}
1561
1562
1563if (GnuPG_version('2.1.0') >= 0) {
1564    my %sockets;
1565    unless ($CONFIG{'no-sign'}) {
1566        # Ensure we have a working agent for our secret key material
1567        my $secdir = $CONFIG{'secret-keyring'};
1568        $secdir =~ s#/[^/]+$## unless -d $secdir;
1569        mysystem('gpg-connect-agent', '--homedir', $secdir, '/bye');
1570        $sockets{'agent-socket'} = GnuPG_version('2.1.13') < 0 ?
1571            # gpgconf < 2.1.13 doesn't understand --homedir; but on
1572            # these versions the gpg-agent socket path is always
1573            # $GNUPGHOME/S.gpg-agent.
1574            "$secdir/S.gpg-agent" :
1575            gpgconf('--homedir', $secdir, '--list-dirs')->{'agent-socket'};
1576    }
1577    unless ($CONFIG{'no-download'}) {
1578        # Ensure we have a working agent for the downloads
1579        my $homedir = $ENV{'GNUPGHOME'} // "$ENV{'HOME'}/.gnupg";
1580        mysystem('gpg-connect-agent', '--homedir', $homedir, '--dirmngr', '/bye');
1581        $sockets{'dirmngr-socket'} = GnuPG_version('2.1.13') < 0 ?
1582            # gpgconf < 2.1.13 doesn't understand --homedir; but on
1583            # these versions the gpg-agent socket path is always
1584            # $GNUPGHOME/S.dirmngr.
1585            "$homedir/S.dirmngr" :
1586            gpgconf('--homedir', $homedir, '--list-dirs')->{'dirmngr-socket'};
1587    }
1588
1589    foreach my $k (keys %sockets) {
1590        my $socket = $sockets{$k};
1591        my $l = GnuPG_version('2.1.13') < 0 ?
1592            $socket =~ s#.*/#$GNUPGHOME/#r :
1593            gpgconf('--homedir', $GNUPGHOME, '--list-dirs')->{$k};
1594        if (-l $l) {
1595            unlink $l
1596        } elsif (-S $l) {
1597            # don't run agents in caff's homedir
1598            myerror(1, "$l: socket exists; runaway gpg-agent?");
1599        } elsif (! -S $socket) {
1600            myerror(1, "Missing socket $socket");
1601        }
1602        debug "Creating symlink $l to $socket";
1603        symlink $socket, $l or myerror(1, "Cannot symlink: $!");
1604    }
1605}
1606elsif ($CONFIG{'also-lsign-in-gnupghome'} eq 'auto' and $CONFIG{'gpg-sign-type'} !~ /l/) {
1607    # Ensure there is a working gpg-agent if $CONFIG{'also-lsign-in-gnupghome'} is 'auto'
1608    system qw/gpg-agent -q/;
1609    unless ($? == 0) {
1610        mywarn("No gpg-agent running: set \$CONFIG{'also-lsign-in-gnupghome'} = 'ask'");
1611        $CONFIG{'also-lsign-in-gnupghome'} = 'ask';
1612    }
1613}
1614
1615##################################
1616# import own keys and keys to sign
1617##################################
1618import_keys_from_gnupghome($CONFIG{'keyid'}, undef, $GNUPGHOME, 1);
1619import_keys_from_gnupghome($CONFIG{'also-encrypt-to'}, undef, $GNUPGHOME, 0) if defined $CONFIG{'also-encrypt-to'};
1620@LOCAL_USER = get_local_user_keys() unless $CONFIG{'no-sign'};
1621
1622import_keys_to_sign(\@KEYIDS);
1623if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) {
1624    $CONFIG{'no-sign'} = ! ask("Continue with signing?", 1);
1625}
1626
1627my %KEYS;
1628for my $keyid (@KEYIDS) {
1629    # get key listing (and ensure there is no collision)
1630    ####################################################
1631    my $gpg = mkGnuPG( homedir => $GNUPGHOME, extra_args => ['--with-fingerprint', '--with-colons'] );
1632    my $handles = mkGnuPG_fds( stdout => undef );
1633
1634    # process the keys one by one so we can detect collisions
1635    my $pid = $gpg->list_public_keys( handles => $handles, command_args => [$keyid] );
1636
1637    my @matching_keys;
1638    while (readline $handles->{stdout}) {
1639        if (/^pub:([^:]+):(?:[^:]*:){2}([0-9A-F]{16}):(?:[^:]*:){6}([^:]+)/) {
1640            push @matching_keys, { validity => $1, longkeyid => $2, capability => $3, uids => [], subkeys => [] };
1641        }
1642        elsif (/^fpr:(?:[^:]*:){8}([0-9A-F]{40}|[0-9A-F]{32})(?::.*)?$/) {
1643            $matching_keys[$#matching_keys]->{fpr} //= $1;
1644        }
1645        elsif (/^sub:[^:]+:(?:[^:]*:){2}([0-9A-F]{16}):/) {
1646            push @{$matching_keys[$#matching_keys]->{subkeys}}, $1;
1647        }
1648        elsif (/^(uid|uat):([^:]+):(?:[^:]*:){5}([0-9A-F]{40}):[^:]*:([^:]+)/) {
1649            my $uid = { type => $1
1650                      , validity => $2
1651                      , hash => $3
1652                      , text => $1 eq 'uid' ? $4 : '[attribute]'
1653            };
1654            $uid->{text} =~ s/\\x(\p{AHex}{2})/ chr(hex($1)) /ge;
1655            # --with-colons always outputs UTF-8
1656            $uid->{text} = Encode::decode_utf8($uid->{text});
1657            $uid->{address} = email_valid $uid->{text} if $uid->{type} eq 'uid';
1658            push @{$matching_keys[$#matching_keys]->{uids}}, $uid;
1659        }
1660        elsif (!/^(?:rvk|tru):/) {
1661            chomp;
1662            mywarn("Got unknown reply from gpg: ".$_);
1663        }
1664    }
1665    done_gpg($pid, $handles);
1666
1667    unless (@matching_keys) {
1668        mywarn("No public keys found with list-key $keyid (note that caff uses its own keyring in $GNUPGHOME)");
1669        next;
1670    }
1671
1672    my $key;
1673    foreach (@matching_keys) {
1674        my $reason = $_->{fpr}        =~ /^\p{AHex}{32}$/ ? 'obsolete v3'
1675                   : $_->{validity}   =~ /e/ ? 'expired'
1676                   : $_->{validity}   =~ /i/ ? 'invalid'
1677                   : $_->{validity}   =~ /r/ ? 'revoked'
1678                   : $_->{capability} =~ /D/ ? 'disabled'
1679                   : do { $key = $_; last };
1680        mywarn("Ignoring $reason key $_->{fpr}");
1681    }
1682    mywarn( "More than one key matched $keyid (assuming $key->{fpr}).  "
1683          . "Try to specify the long keyid or full fingerprint to avoid collisions.")
1684      if $#matching_keys > 0 and defined $key;
1685
1686    if (defined $key) {
1687        $KEYS{$keyid} = $key;
1688    } else {
1689        my $msg = "public key found with list-key $keyid";
1690        mywarn( @matching_keys ? "No valid $msg" : "No $msg  (note that caff uses its own keyring in $GNUPGHOME)" );
1691    }
1692}
1693
1694unless (keys %KEYS) {
1695    notice("No keys to sign found", 0);
1696    exit 0;
1697}
1698
1699
1700for my $keyid (@KEYIDS) {
1701    next unless exists $KEYS{$keyid};
1702    my ($longkeyid, $fpr) = @{$KEYS{$keyid}}{qw/longkeyid fpr/};
1703
1704    ###########
1705    # sign keys
1706    ###########
1707    unless ($CONFIG{'no-sign'}) {
1708        notice("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
1709        foreach my $local_user (@LOCAL_USER) {
1710            my @command = ($CONFIG{'gpg'});
1711            push @command, "--homedir=$GNUPGHOME";
1712            push @command, @GNUPGOPTS if @GNUPGOPTS;
1713            push @command, '--secret-keyring', $CONFIG{'secret-keyring'} if GnuPG_version('2.1.0') < 0;
1714            push @command, qw/--no-auto-check-trustdb --trust-model=always/;
1715            push @command, '--local-user', $local_user;
1716            push @command, '--edit-key', $fpr;
1717            push @command, 'showphoto' if $CONFIG{'show-photos'};
1718            push @command, $CONFIG{'gpg-sign-type'}.'sign';
1719            push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
1720            print join(' ', @command),"\n";
1721            mysystem(@command);
1722        };
1723    };
1724
1725
1726    ##################
1727    # export and prune
1728    ##################
1729
1730    # export the key
1731    ################
1732    my $keydir = File::Temp->newdir( "caff-$keyid-XXXXX", TMPDIR => 1 );
1733    # we can't use only one import here because the cleaning is done as the
1734    # keys come and our keys might not be imported yet
1735    import_keys_from_gnupghome($CONFIG{'keyid'}, $GNUPGHOME, $keydir, 1, 'import-minimal', 'import-local-sigs');
1736    import_keys_from_gnupghome([$fpr],           $GNUPGHOME, $keydir, 1, 'import-clean',   'import-local-sigs');
1737
1738    # the first UID. we won't delete that one when pruning for UATs because a key has to have at least one UID
1739    my @uids = @{$KEYS{$keyid}->{uids}};
1740    my $first_uid = (grep {$_->{type} eq 'uid'} @uids)[0];
1741
1742    for (my $uid_number = 1; $uid_number <= $#uids+1; $uid_number++) {
1743        debug("Doing key $keyid, uid $uid_number");
1744        my $uid = $uids[$uid_number-1];
1745
1746        # /!\ this serial is valid in caff's GnuPGHOME only, and can't
1747        # be relied upon if the keyring is modified in the meantime.
1748        $uid->{serial} = $uid_number;
1749
1750        next if $uid->{validity} =~ /[eir]/; # skip expired / invalid / revokey UIDs
1751
1752        # copy pubring to temporary gpghome
1753        ###################################
1754        my $uiddir = File::Temp->newdir( "caff-$keyid-$uid_number-XXXXX", TMPDIR => 1 );
1755        foreach (qw/pubring.gpg pubring.kbx/) {
1756            copy($keydir.'/'.$_, $uiddir.'/'.$_) if -e $keydir.'/'.$_;
1757        }
1758
1759        # prune it
1760        ##########
1761        my $gpg = mkGnuPG( homedir => $uiddir, extra_args => ['--with-colons'] );
1762        my $handles = mkGnuPG_fds( command => undef, stdout => undef, status => undef );
1763        my $pid = $gpg->wrap_call(
1764            commands     => [ '--edit-key' ],
1765            command_args => [ $fpr ],
1766            handles      => $handles );
1767
1768        debug("Starting edit session");
1769        my %output = readwrite_gpg($handles, status => $KEYEDIT_PROMPT);
1770
1771        # delete other uids
1772        ###################
1773        my $delete_some = 0;
1774        for (my $i = 1; $i <= $#uids+1; $i++) {
1775            # it's quicker with gpg2: 'uid *' then 'uid $i'
1776            next if $i == $uid_number;
1777            next if $uid->{type} ne 'uid' and $uids[$i-1]->{hash} eq $first_uid->{hash}; # keep the first UID
1778
1779            debug("Marking UID $i ($uids[$i-1]->{hash}) for deletion");
1780            readwrite_gpg($handles, command => "uid $i", status => $KEYEDIT_PROMPT);
1781            $delete_some++;
1782        }
1783
1784        if ($delete_some) {
1785            debug("Need to delete $delete_some uids");
1786            readwrite_gpg($handles, command => "deluid", status => $KEYEDIT_DELUID_PROMPT);
1787            readwrite_gpg($handles, command => "yes",    status => $KEYEDIT_PROMPT);
1788        };
1789
1790        # delete all subkeys
1791        ####################
1792        if (@{$KEYS{$keyid}->{subkeys}}) {
1793            for (my $i = 1; $i <= $#{$KEYS{$keyid}->{subkeys}} + 1; $i++) {
1794                debug("Marking subkey $i ($KEYS{$keyid}->{subkeys}->[$i-1]) for deletion");
1795                readwrite_gpg($handles, command => "key $i", status => $KEYEDIT_PROMPT);
1796            };
1797            readwrite_gpg($handles, command => "delkey", status => $KEYEDIT_DELSUBKEY_PROMPT);
1798            readwrite_gpg($handles, command => "yes", status => $KEYEDIT_PROMPT);
1799        };
1800
1801        # delete signatures
1802        ###################
1803        # this shouldn't delete anything as $longkeyid is already clean, but maybe we didn't sign that uid with all keys in @{$CONFIG{'keyid'}}
1804        my ($last_signed_on, $xsigners) = delete_signatures($handles, $longkeyid, $uid->{hash}, $CONFIG{'keyid'});
1805
1806        delete_signatures($handles, $longkeyid, $first_uid->{hash}, [])
1807            if $uid->{type} ne 'uid'; # delete all sigs on the first UID if $uid is an attribute
1808
1809
1810        readwrite_gpg($handles, command => "save");
1811        done_gpg($pid, $handles);
1812        debug("Done editing");
1813
1814        my $asciikey = export_keys($uiddir, [$fpr], 'export-local-sigs');
1815        undef $uiddir; # delete dir
1816
1817        unless ($asciikey) {
1818            mywarn "No data from gpg for export $fpr";
1819            next;
1820        };
1821
1822        if ($last_signed_on) {
1823            # it's a bit inefficient to store the $asciikey in memory,
1824            # but it has been pruned so it's shouldn't be too big
1825            $uid->{key} = $asciikey;
1826            $uid->{xsigners} = $xsigners;
1827            $uid->{last_signed_on} = $last_signed_on;
1828        };
1829    };
1830
1831
1832    unless ($CONFIG{'also-lsign-in-gnupghome'} eq 'no') {
1833        # remove all exportable sigs, and import into our GnuPGHOME
1834        ###########################################################
1835        my $gpg = mkGnuPG( homedir => $keydir, extra_args => ['--with-colons'] );
1836        my $handles = mkGnuPG_fds( command => undef, stdout => undef, status => undef );
1837        my $pid = $gpg->wrap_call(
1838            commands     => [ '--edit-key' ],
1839            command_args => [ $fpr ],
1840            handles      => $handles );
1841
1842        debug("Starting edit session on $keyid");
1843        my %output = readwrite_gpg($handles, status => $KEYEDIT_PROMPT);
1844        delete_signatures($handles, $longkeyid, $uids[$_]->{hash}, $CONFIG{'keyid'}, 1) foreach (0 .. $#uids);
1845
1846        readwrite_gpg($handles, command => "save");
1847        done_gpg($pid, $handles);
1848        debug("Done editing");
1849
1850        # import the pruned keys with our own local sigs only; this is
1851        # required even if there are no lsigs, to ensure we've got all
1852        # UIDs in our own GnuPGHOME
1853        import_keys_from_gnupghome( [$fpr], $keydir, undef, 1, 'import-local-sigs' );
1854    }
1855    undef $keydir; # delete dir
1856
1857    if ($CONFIG{'also-lsign-in-gnupghome'} eq 'ask') {
1858        # manually lsign the key
1859        ########################
1860        foreach my $local_user (@LOCAL_USER) {
1861            my @command = ($CONFIG{'gpg'});
1862            push @command, '--secret-keyring', $CONFIG{'secret-keyring'} if GnuPG_version('2.1.0') < 0;
1863            push @command, qw/--no-auto-check-trustdb --trust-model=always/;
1864            push @command, '--local-user', $local_user;
1865            push @command, '--edit-key', $fpr;
1866            push @command, 'showphoto' if $CONFIG{'show-photos'};
1867            push @command, 'lsign';
1868            push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
1869            print join(' ', @command),"\n";
1870            mysystem(@command);
1871        }
1872    }
1873    elsif ($CONFIG{'also-lsign-in-gnupghome'} eq 'auto') {
1874        # auto lsign the uids we for which we have an exportable sig
1875        ############################################################
1876        my @uids = grep {exists $_->{xsigners}} @{$KEYS{$keyid}->{uids}};
1877        my @signers = map {keys %{$_->{xsigners}}} @uids;
1878        # which of @LOCAL_USER has signed at least one UID in this key?
1879        @signers = grep { my $u = $_; grep { $u eq $_ } @signers } @LOCAL_USER;
1880        @signers = keys %{{ map { $_ => 1 } @signers }}; # remove duplicates to avoid double signing
1881
1882        foreach my $u (@signers) {
1883            my @signeduids; # uids signed by $u
1884            foreach my $uid (@uids) {
1885                # we use UIDs hashes to distinguish and select UIDs; it's the only reliable way to identify them across keyrings
1886                push @signeduids, $uid if grep { $u eq $_ } (keys %{$uid->{xsigners}}) and
1887                                         !grep { $uid->{hash} eq $_->{hash} } @signeduids;
1888            }
1889
1890            my $gpg = mkGnuPG( extra_args => ['--local-user' => $u, '--ask-cert-level', '--with-colons', '--no-batch'] );
1891            $gpg->options->push_extra_args('--secret-keyring', $CONFIG{'secret-keyring'}) if GnuPG_version('2.1.0') < 0;
1892            $gpg->options->push_extra_args('--use-agent') if GnuPG_version('2.0.0') < 0; # we know there is a working agent
1893            my $handles = mkGnuPG_fds( command => undef, stdout => undef, status => undef );
1894            my $pid = $gpg->wrap_call(
1895                commands     => [ '--edit-key' ],
1896                command_args => [ $fpr ],
1897                handles      => $handles );
1898
1899            debug("Starting edit session on $keyid, signer $u");
1900            readwrite_gpg($handles, status => $KEYEDIT_PROMPT);
1901
1902            foreach my $level (0..3) {
1903                my @signeduids_with_level = grep {$_->{xsigners}->{$u} eq $level} @signeduids;
1904                next unless @signeduids_with_level;
1905
1906                notice("Key $longkeyid UID(s) #".(join ',', sort (map {$_->{serial}} @signeduids_with_level)).": lsign'ing with $u, cert level $level", 1);
1907                readwrite_gpg($handles, command => "uid 0",          status => $KEYEDIT_PROMPT); # unselect UIDs
1908                readwrite_gpg($handles, command => "uid $_->{hash}", status => $KEYEDIT_PROMPT) for @signeduids_with_level;
1909                my %output = readwrite_gpg($handles, command => "lsign", status => qr/$KEYEDIT_SIGNUID_CLASS_PROMPT|$KEYEDIT_PROMPT/);
1910                next if $output{status} =~ /^\[GNUPG:\] $KEYEDIT_PROMPT/m; # already signed
1911                readwrite_gpg($handles, command => $level, status => $KEYEDIT_SIGNUID_PROMPT);
1912                readwrite_gpg($handles, command => "yes",  status => $KEYEDIT_PROMPT);
1913            }
1914
1915            readwrite_gpg($handles, command => "save");
1916            done_gpg($pid, $handles);
1917            debug("Done editing");
1918        }
1919    }
1920}
1921
1922#############
1923# send emails
1924#############
1925for my $keyid (@KEYIDS) {
1926    next unless exists $KEYS{$keyid};
1927    my $longkeyid = $KEYS{$keyid}->{longkeyid};
1928    my $can_encrypt = $KEYS{$keyid}->{capability} =~ /E/;
1929    my @uids = @{$KEYS{$keyid}->{uids}};
1930
1931    unless (grep {$_->{last_signed_on}} @uids) {
1932        info("Key 0x$longkeyid has no signed uids, skipping", 0);
1933        next;
1934    }
1935
1936    my @attached;
1937    for my $uid (@uids) {
1938        my $text = defined $LOCALE ? $LOCALE->encode($uid->{text}) : $uid->{text};
1939
1940        trace("UID: $text\n");
1941        if ($uid->{validity} =~ /[eir]/) {
1942            my $reason = $uid->{validity} =~ /e/ ? 'expired' :
1943                         $uid->{validity} =~ /i/ ? 'invalid' :
1944                         $uid->{validity} =~ /r/ ? 'revoked' : die;
1945            info("Key 0x$longkeyid ".(uc $uid->{type})." $uid->{serial} $text is $reason, skipping", 0);
1946            next;
1947        }
1948        unless ($uid->{last_signed_on}) {
1949            info("Key 0x$longkeyid ".(uc $uid->{type})." $uid->{serial} $text is not signed by me, skipping", 0);
1950            next;
1951        }
1952
1953        if ($NOW - $uid->{last_signed_on} > $CONFIG{'export-sig-age'} and
1954                !ask("Signature on $text is old.  Export?", 0, $PARAMS->{'export-old'}, $PARAMS->{'no-export-old'})) {
1955            next;
1956        }
1957
1958        # save the armored key
1959        my $keydir = "$KEYSBASE/$DATE_STRING";
1960        -d $keydir || mkdir $keydir, 0700 or myerror(1, "Cannot mkdir $keydir: $!");
1961
1962        my $keyfile = "$keydir/$longkeyid.key.$uid->{serial}.".sanitize_uid($text).".asc";
1963        open my $KEY, '>', $keyfile or myerror(1, "Cannot open $keyfile: $!");
1964        debug "Writing armored key 0x$longkeyid to $keyfile";
1965        print $KEY $uid->{key};
1966        close $KEY;
1967
1968        if ($uid->{type} eq 'uat') {
1969            if (ask("UID $text is an attribute UID, attach it to every email?", 1)) {
1970                push @attached, $uid;
1971                $uid->{export} = 1;
1972            }
1973        } elsif (!defined $uid->{address}) {
1974            if (ask("UID $text is no email address, attach it to every email?", 1)) {
1975                push @attached, $uid;
1976                $uid->{export} = 1;
1977            }
1978        }
1979        else {
1980            $uid->{export} = 1;
1981        }
1982
1983        info("Key 0x$longkeyid ".(uc $uid->{type})." $uid->{serial} $text done", 1);
1984    }
1985
1986    @uids = grep {$_->{last_signed_on}} @uids;              # ignore UIDs we didn't sign
1987    delete $_->{key} foreach grep {!$_->{export}} @uids;    # delete non-exported keys
1988
1989    if (!grep {defined $_->{address}} @uids) {
1990        mywarn "No signed RFC 5322 UID on $longkeyid; won't send other signed UID and attributes!"
1991            if @attached;
1992    }
1993    elsif (grep {$_->{export}} @uids) {
1994        notice("Key 0x$longkeyid has no encryption capabilities, mail(s) will be sent/stored unencrypted", 0) unless $can_encrypt;
1995        my $sendmail = $can_encrypt ? $CONFIG{'mail'} : $CONFIG{'mail-cant-encrypt'};
1996
1997        for my $uid (@uids) {
1998            next unless defined $uid->{address};
1999            next unless $uid->{export} or @attached;
2000            my @keys = @attached;
2001            unshift @keys, $uid if exists $uid->{key};
2002
2003            my $mail = create_mail($uid->{address}, $can_encrypt, $longkeyid, @keys);
2004            if (defined $mail) {
2005                my @sentuids = map {$_->{text}} @attached;
2006                unshift @sentuids, $uid->{text} if $uid->{export};
2007                do { $_ = $LOCALE->encode($_) foreach @sentuids; } if defined $LOCALE;
2008                my $text = join(', ', map {"'$_'"} @sentuids);
2009
2010                my $should_send_mail = ask("Mail ".($can_encrypt ? '' : '*unencrypted* ')."signature for $text to '$uid->{address}'?",
2011                                           $sendmail ne 'ask-no', $sendmail eq 'yes', $sendmail eq 'no');
2012                send_message($mail) if $should_send_mail;
2013
2014                my $keydir = "$KEYSBASE/$DATE_STRING";
2015                my $mailfile = "$keydir/$longkeyid.mail.".($should_send_mail ? '' : 'unsent.').$uid->{'serial'}.".".sanitize_uid($text);
2016                open my $MAILFILE, '>', $mailfile or myerror(1, "Cannot open $mailfile: $!");
2017                debug "Writing message to $mailfile";
2018                $mail->print($MAILFILE);
2019                close $MAILFILE;
2020            } else {
2021                mywarn "Generating mail failed";
2022            }
2023        }
2024    }
2025
2026    info("Key 0x$longkeyid done", 1);
2027}
2028
2029END {
2030    if (defined $TERMIOS and defined (my $fd = fileno(TTY))) {
2031        $TERMIOS->setattr($fd, POSIX::TCSANOW) // warn "setattr: $!";
2032    }
2033}
2034
2035###########################
2036# the default mail template
2037###########################
2038
2039__DATA__
2040Hi,
2041
2042please find attached the user id{(scalar @uids >= 2 ? 's' : '')}
2043{foreach $uid (@uids) {
2044    $OUT .= "\t".$uid."\n";
2045};}of your key {$key} signed by me.
2046
2047If you have multiple user ids, I sent the signature for each user id
2048separately to that user id's associated email address. You can import
2049the signatures by running each through `gpg --import`.
2050
2051Note that I did not upload your key to any keyservers. If you want this
2052new signature to be available to others, please upload it yourself.
2053With GnuPG this can be done using
2054	gpg --keyserver hkp://pool.sks-keyservers.net --send-key {$key}
2055
2056If you have any questions, don't hesitate to ask.
2057
2058Regards,
2059--
2060{$owner}
2061