Minor fix for pesky v3k keys
[pgp-tools.git] / caff / caff
1 #!/usr/bin/perl -w
2
3 # caff -- CA - Fire and Forget
4 # $Id$
5 #
6 # Copyright (c) 2004, 2005 Peter Palfrader <peter@palfrader.org>
7 # Copyright (c) 2005 Christoph Berg <cb@df7cb.de>
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
37 caff -- CA - Fire and Forget
38
39 =head1 SYNOPSIS
40
41 =over
42
43 =item B<caff> [-eEmMRS] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
44
45 =back
46
47 =head1 DESCRIPTION
48
49 CA Fire and Forget is a script that helps you in keysigning. It takes a list
50 of keyids on the command line, fetches them from a keyserver and calls GnuPG so
51 that you can sign it. It then mails each key to all its email addresses - only
52 including the one UID that we send to in each mail, pruned from all but self
53 sigs and sigs done by you. The mailed key is encrypted with itself as a means
54 to verify that key belongs to the recipient.
55
56 =head1 OPTIONS
57
58 =over
59
60 =item B<-e>, B<--export-old>
61
62 Export old signatures. Default is to ask the user for each old signature.
63
64 =item B<-E>, B<--no-export-old>
65
66 Do not export old signatures. Default is to ask the user for each old
67 signature.
68
69 =item B<-m>, B<--mail>
70
71 Send mail after signing. Default is to ask the user for each uid.
72
73 =item B<-M>, B<--no-mail>
74
75 Do not send mail after signing. Default is to ask the user for each uid.
76
77 =item B<-R>, B<--no-download>
78
79 Do not retrieve the key to be signed from a keyserver.
80
81 =item B<-S>, B<--no-sign>
82
83 Do not sign the keys.
84
85 =item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
86
87 Select the key that is used for signing, in case you have more than one key.
88
89 =item B<--key-file> I<file>
90
91 Import keys from file. Can be supplied more than once.
92
93 =back
94
95 =head1 FILES
96
97 =over
98
99 =item $HOME/.caffrc - configuration file
100
101 =item $HOME/.caff/keys/yyyy-mm-dd/ - processed keys
102
103 =item $HOME/.caff/gnupghome/ - caff's working dir for gpg
104
105 =item $HOME/.caff/gnupghome/gpg.conf - gpg configuration
106
107 useful options include use-agent, default-cert-level, etc.
108
109 =back
110
111 =head1 CONFIGURATION FILE OPTIONS
112
113 The configuration file is a perl script that sets values in the hash B<%CONFIG>.
114 The file is generated when it does not exist.
115
116 Example:
117
118 $CONFIG{owner} = q{Peter Palfrader};
119 $CONFIG{email} = q{peter@palfrader.org};
120 $CONFIG{keyid} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
121
122 =head2 Required basic settings
123
124 =over
125
126 =item B<owner> [string]
127
128 Your name. B<REQUIRED>.
129
130 =item B<email> [string]
131
132 Your email address, used in From: lines. B<REQUIRED>.
133
134 =item B<keyid> [list of keyids]
135
136 A list of your keys. This is used to determine which signatures to keep
137 in the pruning step. If you select a key using B<-u> it has to be in
138 this list. B<REQUIRED>.
139
140 =head2 General settings
141
142 =item B<caffhome> [string]
143
144 Base directory for the files caff stores. Default: B<$HOME/.caff/>.
145
146 =head2 GnuPG settings
147
148 =item B<gpg> [string]
149
150 Path to the GnuPG binary. Default: B<gpg>.
151
152 =item B<gpg-sign> [string]
153
154 Path to the GnuPG binary which is used to sign keys. Default: what
155 B<gpg> is set to.
156
157 =item B<gpg-delsig> [string]
158
159 Path to the GnuPG binary which is used to split off signatures. This was
160 needed while the upstream GnuPG was not fixed. Default: what B<gpg>
161 is set to.
162
163 =item B<secret-keyring> [string]
164
165 Path to your secret keyring. Default: B<$HOME/.gnupg/secring.gpg>.
166
167 =item B<also-encrypt-to> [keyid]
168
169 An additional keyid to encrypt messages to. Default: none.
170
171 =item B<gpg-sign-args> [string]
172
173 Additional arguments to pass to gpg. Default: none.
174
175 =head2 Keyserver settings
176
177 =item B<keyserver> [string]
178
179 Keyserver to download keys from. Default: B<subkeys.pgp.net>.
180
181 =item B<no-download> [boolean]
182
183 If true, then skip the step of fetching keys from the keyserver.
184 Default: B<0>.
185
186 =item B<key-files> [list of files]
187
188 A list of files containing keys to be imported.
189
190 =head2 Signing settings
191
192 =item B<no-sign> [boolean]
193
194 If true, then skip the signing step. Default: B<0>.
195
196 =item B<ask-sign> [boolean]
197
198 If true, then pause before continuing to the signing step.
199 This is useful for offline signing. Default: B<0>.
200
201 =item B<export-sig-age> [seconds]
202
203 Don't export UIDs by default, on which your latest signature is older
204 than this age. Default: B<24*60*60> (i.e. one day).
205
206 =head2 Mail settings
207
208 =item B<mail> [boolean]
209
210 Do not prompt for sending mail, just do it. Default: B<0>.
211
212 =item B<no-mail> [boolean]
213
214 Do not prompt for sending mail. The messages are still written to
215 $CONFIG{caffhome}/keys/. Default: B<0>.
216
217 =item B<mail-template> [string]
218
219 Email template which is used as the body text for the email sent out
220 instead of the default text if specified. The following perl variables
221 can be used in the template:
222
223 =over
224
225 =item B<{owner}> [string]
226
227 Your name as specified in the L<B<owner>|/item_owner__5bstring_5d> setting.
228
229 =item B<{key}> [string]
230
231 The keyid of the key you signed.
232
233 =item B<{@uids}> [array]
234
235 The UIDs for which signatures are included in the mail.
236
237 =back
238
239 =item B<reply-to> [string]
240
241 Add a Reply-To: header to messages sent. Default: none.
242
243 =item B<bcc> [string]
244
245 Address to send blind carbon copies to when sending mail.
246 Default: none.
247
248 =item B<mailer-send> [array]
249
250 Parameters to pass to Mail::Mailer.
251 This could for example be
252
253 $CONFIG{mailer-send} = [ 'smtp', Server => 'mail.server', Auth => ['user', 'pass'] ]
254
255 to use the perl SMTP client or
256
257 $CONFIG{mailer-send} = [ 'sendmail', '-o8' ]
258
259 to pass arguments to the sendmail program.
260 For more information run C<< perldoc Mail::Mailer >>.
261 Setting this option is strongly discouraged. Fix your local MTA
262 instead.
263 Default: none.
264
265 =back
266
267 =head1 AUTHORS
268
269 =over
270
271 =item Peter Palfrader <peter@palfrader.org>
272
273 =item Christoph Berg <cb@df7cb.de>
274
275 =back
276
277 =head1 WEBSITE
278
279 http://pgp-tools.alioth.debian.org/
280
281 =head1 SEE ALSO
282
283 gpg(1), pgp-clean(1), /usr/share/doc/signing-party/caff/caffrc.sample.
284
285 =cut
286
287 use strict;
288 use IO::Handle;
289 use English;
290 use File::Path;
291 use File::Temp qw{tempdir};
292 use Text::Template;
293 use MIME::Entity;
294 use Fcntl;
295 use IO::Select;
296 use Getopt::Long;
297 use GnuPG::Interface;
298
299 my %CONFIG;
300 my $REVISION = '$Rev$';
301 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
302 my $VERSION = "0.0.0.$REVISION_NUMER";
303
304
305
306 sub mywarn($) {
307 my ($line) = @_;
308 print "[WARN] $line\n";
309 };
310 sub notice($) {
311 my ($line) = @_;
312 print "[NOTICE] $line\n";
313 };
314 sub info($) {
315 my ($line) = @_;
316 print "[INFO] $line\n";
317 };
318 sub debug($) {
319 my ($line) = @_;
320 #print "[DEBUG] $line\n";
321 };
322 sub trace($) {
323 my ($line) = @_;
324 #print "[trace] $line\n";
325 };
326 sub trace2($) {
327 my ($line) = @_;
328 #print "[trace2] $line\n";
329 };
330
331
332 sub generate_config() {
333 notice("Error: \$LOGNAME is not set.\n") unless defined $ENV{'LOGNAME'};
334 my $gecos = defined $ENV{'LOGNAME'} ? (getpwnam($ENV{LOGNAME}))[6] : undef;
335 my $email;
336 my @keys;
337 my $hostname = `hostname -f`;
338 chomp $hostname;
339 my ($Cgecos,$Cemail,$Ckeys) = ('','','');
340 if (defined $gecos) {
341 $gecos =~ s/,.*//;
342
343 my $gpg = GnuPG::Interface->new();
344 $gpg->call( 'gpg' );
345 $gpg->options->hash_init(
346 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode } ] );
347 $gpg->options->meta_interactive( 0 );
348 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
349 my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $gecos ]);
350 my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
351 waitpid $pid, 0;
352
353 if ($stdout eq '') {
354 warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
355 };
356
357 @keys = ($stdout =~ /^pub:[^r:]*:(?:[^:]*:){2,2}([^:]+):/mg);
358 unless (scalar @keys) {
359 info("Error: No keys were found using \"gpg --list-public-keys '$gecos'\".");
360 @keys = qw{0123456789abcdef 89abcdef76543210};
361 $Ckeys = '#';
362 }
363 ($email) = ($stdout =~ /^uid:.*<(.+?@.+?)>.*:/m);
364 unless (defined $email) {
365 info("Error: No email address was found using \"gpg --list-public-keys '$gecos'\".");
366 $email = $ENV{'LOGNAME'}.'@'.$hostname;
367 $Cemail = '#';
368 }
369 } else {
370 $gecos = 'Unknown Caff User';
371 $email = $ENV{'LOGNAME'}.'@'.$hostname;
372 @keys = qw{0123456789abcdef 89abcdef76543210};
373 ($Cgecos,$Cemail,$Ckeys) = ('#','#','#');
374 };
375
376 return <<EOT;
377 # .caffrc -- vim:syntax=perl:
378 # This file is in perl(1) format - see caff(1) for details.
379
380 $Cgecos\$CONFIG{'owner'} = '$gecos';
381 $Cemail\$CONFIG{'email'} = '$email';
382
383 # you can get your long keyid from
384 # gpg --with-colons --list-key <yourkeyid|name|emailaddress..>
385 #
386 # if you have a v4 key, it will simply be the last 16 digits of
387 # your fingerprint.
388 #
389 # Example:
390 # \$CONFIG{'keyid'} = [ qw{FEDCBA9876543210} ];
391 # or, if you have more than one key:
392 # \$CONFIG{'keyid'} = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
393
394 $Ckeys\$CONFIG{'keyid'} = [ qw{@keys} ];
395 EOT
396 };
397
398 sub check_executable($$) {
399 # (GnuPG::Interface gives lousy errors when the gpg binary isn't found,
400 # so we want to check manually.)
401 my ($purpose, $fn) = @_;
402 # Only check provided fnames with a slash in them.
403 return unless defined $fn;
404 if ($fn =~ m!/!) {
405 die ("$PROGRAM_NAME: $purpose executable '$fn' not found.\n") unless -x $fn;
406 } else {
407 for my $p (split(':', $ENV{PATH})) {
408 return if -x "$p/$fn";
409 };
410 die ("$PROGRAM_NAME: $purpose executable '$fn' not found on path.\n") unless -x $fn;
411 };
412 };
413
414 sub load_config() {
415 my $config = $ENV{'HOME'} . '/.caffrc';
416 unless (-f $config) {
417 print "No configfile $config present, I will use this template:\n";
418 my $template = generate_config();
419 print "$template\nPlease edit $config and run caff again.\n";
420 open F, ">$config" or die "$config: $!";
421 print F $template;
422 close F;
423 exit(1);
424 }
425 unless (scalar eval `cat $config`) {
426 die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
427 };
428
429 $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'};
430 die ("$PROGRAM_NAME: owner is not defined in $config.\n") unless defined $CONFIG{'owner'};
431 die ("$PROGRAM_NAME: email is not defined in $config.\n") unless defined $CONFIG{'email'};
432 die ("$PROGRAM_NAME: keyid is not defined in $config.\n") unless defined $CONFIG{'keyid'};
433 die ("$PROGRAM_NAME: keyid is not an array ref in $config.\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
434 for my $keyid (@{$CONFIG{'keyid'}}) {
435 $keyid =~ /^[A-F0-9]{16}$/i or die ("$PROGRAM_NAME: key $keyid is not a long (16 digit) keyid in $config.\n");
436 };
437 @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}};
438 $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
439 $CONFIG{'keyserver'} = 'subkeys.pgp.net' unless defined $CONFIG{'keyserver'};
440 $CONFIG{'gpg'} = 'gpg' unless defined $CONFIG{'gpg'};
441 $CONFIG{'gpg-sign'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-sign'};
442 $CONFIG{'gpg-delsig'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-delsig'};
443 check_executable("gpg", $CONFIG{'gpg'});
444 check_executable("gpg-sign", $CONFIG{'gpg-sign'});
445 check_executable("gpg-delsig", $CONFIG{'gpg-delsig'});
446 $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
447 $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
448 $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
449 $CONFIG{'key-files'} = () unless defined $CONFIG{'key-files'};
450 $CONFIG{'mailer-send'} = [] unless defined $CONFIG{'mailer-send'};
451 die ("$PROGRAM_NAME: mailer-send is not an array ref in $config.\n") unless (ref $CONFIG{'mailer-send'} eq 'ARRAY');
452 $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
453 Hi,
454
455 please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
456 {foreach $uid (@uids) {
457 $OUT .= "\t".$uid."\n";
458 };} of your key {$key} signed by me.
459
460 Note that I did not upload your key to any keyservers.
461 If you have multiple user ids, I sent the signature for each user id
462 separately to that user id's associated email address. You can import
463 the signatures by running each through `gpg --import`.
464
465 If you want this new signature to be available to others, please upload
466 it yourself. With GnuPG this can be done using
467 gpg --keyserver subkeys.pgp.net --send-key {$key}
468
469 If you have any questions, don't hesitate to ask.
470
471 Regards,
472 {$owner}
473 EOM
474 };
475
476 sub make_gpg_fds() {
477 my %fds = (
478 stdin => IO::Handle->new(),
479 stdout => IO::Handle->new(),
480 stderr => IO::Handle->new(),
481 status => IO::Handle->new() );
482 my $handles = GnuPG::Handles->new( %fds );
483 return ($fds{'stdin'}, $fds{'stdout'}, $fds{'stderr'}, $fds{'status'}, $handles);
484 };
485
486 sub readwrite_gpg($$$$$%) {
487 my ($in, $inputfd, $stdoutfd, $stderrfd, $statusfd, %options) = @_;
488
489 trace("Entering readwrite_gpg.");
490
491 my ($first_line, undef) = split /\n/, $in;
492 debug("readwrite_gpg sends ".(defined $first_line ? $first_line : "<nothing>"));
493
494 local $INPUT_RECORD_SEPARATOR = undef;
495 my $sout = IO::Select->new();
496 my $sin = IO::Select->new();
497 my $offset = 0;
498
499 trace("input is $inputfd; output is $stdoutfd; err is $stderrfd; status is ".(defined $statusfd ? $statusfd : 'undef').".");
500
501 $inputfd->blocking(0);
502 $stdoutfd->blocking(0);
503 $statusfd->blocking(0) if defined $statusfd;
504 $stderrfd->blocking(0);
505 $sout->add($stdoutfd);
506 $sout->add($stderrfd);
507 $sout->add($statusfd) if defined $statusfd;
508 $sin->add($inputfd);
509
510 my ($stdout, $stderr, $status) = ("", "", "");
511 my $exitwhenstatusmatches = $options{'exitwhenstatusmatches'};
512 trace("doing stuff until we find $exitwhenstatusmatches") if defined $exitwhenstatusmatches;
513
514 my $readwrote_stuff_this_time = 0;
515 my $do_not_wait_on_select = 0;
516 my ($readyr, $readyw, $written);
517 while ($sout->count() > 0 || (defined($sin) && ($sin->count() > 0))) {
518 if (defined $exitwhenstatusmatches) {
519 if ($status =~ /$exitwhenstatusmatches/m) {
520 trace("readwrite_gpg found match on $exitwhenstatusmatches");
521 if ($readwrote_stuff_this_time) {
522 trace("read/write some more\n");
523 $do_not_wait_on_select = 1;
524 } else {
525 trace("that's it in our while loop.\n");
526 last;
527 }
528 };
529 };
530
531 $readwrote_stuff_this_time = 0;
532 trace("select waiting for ".($sout->count())." fds.");
533 ($readyr, $readyw, undef) = IO::Select::select($sout, $sin, undef, $do_not_wait_on_select ? 0 : 1);
534 trace("ready: write: ".(defined $readyw ? scalar @$readyw : 0 )."; read: ".(defined $readyr ? scalar @$readyr : 0));
535 for my $wfd (@$readyw) {
536 $readwrote_stuff_this_time = 1;
537 if (length($in) != $offset) {
538 trace("writing to $wfd.");
539 $written = $wfd->syswrite($in, length($in) - $offset, $offset);
540 $offset += $written;
541 };
542 if ($offset == length($in)) {
543 trace("writing to $wfd done.");
544 unless ($options{'nocloseinput'}) {
545 close $wfd;
546 trace("$wfd closed.");
547 };
548 $sin->remove($wfd);
549 $sin = undef;
550 }
551 }
552
553 next unless (defined(@$readyr)); # Wait some more.
554
555 for my $rfd (@$readyr) {
556 $readwrote_stuff_this_time = 1;
557 if ($rfd->eof) {
558 trace("reading from $rfd done.");
559 $sout->remove($rfd);
560 close($rfd);
561 next;
562 }
563 trace("reading from $rfd.");
564 if ($rfd == $stdoutfd) {
565 $stdout .= <$rfd>;
566 trace2("stdout is now $stdout\n================");
567 next;
568 }
569 if (defined $statusfd && $rfd == $statusfd) {
570 $status .= <$rfd>;
571 trace2("status is now $status\n================");
572 next;
573 }
574 if ($rfd == $stderrfd) {
575 $stderr .= <$rfd>;
576 trace2("stderr is now $stderr\n================");
577 next;
578 }
579 }
580 }
581 trace("readwrite_gpg done.");
582 return ($stdout, $stderr, $status);
583 };
584
585 sub ask($$;$$) {
586 my ($question, $default, $forceyes, $forceno) = @_;
587 my $answer;
588 my $yn = $default ? '[Y/n]' : '[y/N]';
589 while (1) {
590 print $question,' ',$yn, ' ';
591 if ($forceyes && $forceno) {
592 print "$default (from config/command line)\n";
593 return $default;
594 };
595 if ($forceyes) {
596 print "YES (from config/command line)\n";
597 return 1;
598 };
599 if ($forceno) {
600 print "NO (from config/command line)\n";
601 return 0;
602 };
603
604 $answer = <STDIN>;
605 if (!defined $answer) {
606 $OUTPUT_AUTOFLUSH = 1;
607 die "\n\n".
608 "End of STDIN reached. Are you using xargs? Caff wants to read from STDIN,\n".
609 "so you can't really use it with xargs. A patch against caff to read from\n".
610 "the terminal would be appreciated.\n".
611 "For now instead of cat keys | xargs caff do caff `cat keys`\n";
612 };
613 chomp $answer;
614 last if ((length $answer == 0) || ($answer =~ m/^[yYnN]$/) );
615 print "What about $yn is so hard to understand?\nAnswer with either 'n' or 'y' or just press enter for the default.\n";
616 sleep 1;
617 };
618 my $result = $default;
619 $result = 1 if $answer =~ /y/i;
620 $result = 0 if $answer =~ /n/i;
621 return $result;
622 };
623
624
625
626
627
628 my $KEYEDIT_PROMPT = '^\[GNUPG:\] GET_LINE keyedit.prompt';
629 my $KEYEDIT_DELUID_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.uid.okay';
630 my $KEYEDIT_DELSIG_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.delsig';
631 my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = '^\[GNUPG:\] (GET_BOOL keyedit.delsig|GET_LINE keyedit.prompt)';
632 my $KEYEDIT_DELSUBKEY_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.subkey';
633
634 load_config;
635 my $USER_AGENT = "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.";
636
637 my $KEYSBASE = $CONFIG{'caffhome'}.'/keys';
638 my $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
639
640 -d $KEYSBASE || mkpath($KEYSBASE , 0, 0700) or die ("Cannot create $KEYSBASE: $!\n");
641 -d $GNUPGHOME || mkpath($GNUPGHOME, 0, 0700) or die ("Cannot create $GNUPGHOME: $!\n");
642
643 my $NOW = time;
644 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($NOW);
645 my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday);
646
647
648 sub version($) {
649 my ($fd) = @_;
650 print $fd "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.\n";
651 };
652
653 sub usage($$) {
654 my ($fd, $exitcode) = @_;
655 version($fd);
656 print $fd "Usage: $PROGRAM_NAME [-eEmMRS] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
657 print $fd "Consult the manual page for more information.\n";
658 exit $exitcode;
659 };
660
661 ######
662 # export key $keyid from $gnupghome
663 ######
664 sub export_key($$) {
665 my ($gnupghome, $keyid) = @_;
666
667 my $gpg = GnuPG::Interface->new();
668 $gpg->call( $CONFIG{'gpg'} );
669 if (defined $gnupghome) {
670 $gpg->options->hash_init(
671 'homedir' => $gnupghome,
672 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ],
673 'armor' => 1 );
674 } else {
675 $gpg->options->hash_init(
676 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ],
677 'armor' => 1 );
678 };
679 $gpg->options->meta_interactive( 0 );
680 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
681 my $pid = $gpg->export_keys(handles => $handles, command_args => [ $keyid ]);
682 my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
683 waitpid $pid, 0;
684
685 return $stdout;
686 };
687
688 ######
689 # import a key from the scalar $asciikey into a gpg homedirectory in $tempdir
690 ######
691 sub import_key($$) {
692 my ($gnupghome, $asciikey) = @_;
693
694 my $gpg = GnuPG::Interface->new();
695 $gpg->call( $CONFIG{'gpg'} );
696 $gpg->options->hash_init(
697 'homedir' => $gnupghome,
698 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ] );
699 $gpg->options->meta_interactive( 0 );
700 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
701 my $pid = $gpg->import_keys(handles => $handles);
702 my ($stdout, $stderr, $status) = readwrite_gpg($asciikey, $inputfd, $stdoutfd, $stderrfd, $statusfd);
703 waitpid $pid, 0;
704
705 if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
706 return undef;
707 };
708 return 1;
709 };
710
711
712 ######
713 # Send an email to $address. If $can_encrypt is true then the mail
714 # will be PGP/MIME encrypted to $longkeyid.
715 #
716 # $longkeyid, $uid, and @attached will be used in the email and the template.
717 ######
718 #send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
719 sub send_mail($$$@) {
720 my ($address, $can_encrypt, $key_id, @keys) = @_;
721
722 my $template = Text::Template->new(TYPE => 'STRING', SOURCE => $CONFIG{'mail-template'})
723 or die "Error creating template: $Text::Template::ERROR";
724
725 my @uids;
726 for my $key (@keys) {
727 push @uids, $key->{'text'};
728 };
729 my $message = $template->fill_in(HASH => { key => $key_id,
730 uids => \@uids,
731 owner => $CONFIG{'owner'}})
732 or die "Error filling template in: $Text::Template::ERROR";
733
734 my $message_entity = MIME::Entity->build(
735 Type => "text/plain",
736 Charset => "utf-8",
737 Disposition => 'inline',
738 Data => $message);
739
740 my @key_entities;
741 for my $key (@keys) {
742 $message_entity->attach(
743 Type => "application/pgp-keys",
744 Disposition => 'attachment',
745 Encoding => "7bit",
746 Description => "PGP Key 0x$key_id, uid ".($key->{'text'}).' ('.($key->{'serial'}).'), signed by 0x'.$CONFIG{'keyid'}[0],
747 Data => $key->{'key'},
748 Filename => "0x$key_id.".$key->{'serial'}.".signed-by-0x".$CONFIG{'keyid'}[0].".asc");
749 };
750
751 if ($can_encrypt) {
752 my $message = $message_entity->stringify();
753
754 my $gpg = GnuPG::Interface->new();
755 $gpg->call( $CONFIG{'gpg'} );
756 $gpg->options->hash_init( 'homedir' => $GNUPGHOME,
757 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ],
758 'armor' => 1 );
759 $gpg->options->meta_interactive( 0 );
760 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
761 $gpg->options->push_recipients( $key_id );
762 $gpg->options->push_recipients( $CONFIG{'also-encrypt-to'} ) if defined $CONFIG{'also-encrypt-to'};
763 my $pid = $gpg->encrypt(handles => $handles);
764 my ($stdout, $stderr, $status) = readwrite_gpg($message, $inputfd, $stdoutfd, $stderrfd, $statusfd);
765 waitpid $pid, 0;
766 if ($stdout eq '') {
767 warn ("No data from gpg for list-key $key_id\n");
768 next;
769 };
770 $message = $stdout;
771
772 $message_entity = MIME::Entity->build(
773 Type => 'multipart/encrypted; protocol="application/pgp-encrypted"',
774 Encoding => '7bit');
775
776 $message_entity->attach(
777 Type => "application/pgp-encrypted",
778 Disposition => 'attachment',
779 Encoding => "7bit",
780 Data => "Version: 1\n");
781
782 $message_entity->attach(
783 Type => "application/octet-stream",
784 Filename => 'msg.asc',
785 Disposition => 'inline',
786 Encoding => "7bit",
787 Data => $message);
788 };
789
790 $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
791 $message_entity->head->add("To", $address);
792 $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
793 $message_entity->head->add("Reply-To", $CONFIG{'reply-to'}) if defined $CONFIG{'reply-to'};
794 $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
795 $message_entity->head->add("User-Agent", $USER_AGENT);
796 mywarn("You have set arguments to pass to Mail::Mailer. Better fix your MTA. (Also, Mail::Mailer's error reporting is non existant, so it won't tell you when it doesn't work.)") if (scalar @{$CONFIG{'mailer-send'}} > 0);
797 $message_entity->send(@{$CONFIG{'mailer-send'}});
798 $message_entity->stringify();
799 };
800
801 ######
802 # clean up a UID so that it can be used on the FS.
803 ######
804 sub sanitize_uid($) {
805 my ($uid) = @_;
806
807 my $good_uid = $uid;
808 $good_uid =~ tr#/:\\#_#;
809 trace2("[sanitize_uid] changed UID from $uid to $good_uid.\n") if $good_uid ne $uid;
810 return $good_uid;
811 };
812
813 sub delete_signatures($$$$$$) {
814 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $keyids) =@_;
815
816 my $signed_by_me = 0;
817
818 my ($stdout, $stderr, $status) =
819 readwrite_gpg("delsig\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_DELSIG_PROMPT, nocloseinput => 1);
820
821 while($status =~ /$KEYEDIT_DELSIG_PROMPT/m) {
822 # sig:?::17:EA2199412477CAF8:1058095214:::::13x:
823 my @sigline = grep { /^sig/ } (split /\n/, $stdout);
824 $stdout =~ s/\n/\\n/g;
825 notice("[sigremoval] why are there ".(scalar @sigline)." siglines in that part of the dialog!? got: $stdout") if scalar @sigline >= 2; # XXX
826 my $line = pop @sigline;
827 my $answer = "no";
828 if (defined $line) { # only if we found a sig here - we never remove revocation packets for instance
829 debug("[sigremoval] doing line $line.");
830 my (undef, undef, undef, undef, $signer, $created, undef, undef, undef) = split /:/, $line;
831 if ($signer eq $longkeyid) {
832 debug("[sigremoval] selfsig ($signer).");
833 $answer = "no";
834 } elsif (grep { $signer eq $_ } @{$keyids}) {
835 debug("[sigremoval] signed by us ($signer).");
836 $answer = "no";
837 $signed_by_me = $signed_by_me > $created ? $signed_by_me : $created;
838 } else {
839 debug("[sigremoval] not interested in that sig ($signer).");
840 $answer = "yes";
841 };
842 } else {
843 debug("[sigremoval] no sig line here, only got: ".$stdout);
844 };
845 ($stdout, $stderr, $status) =
846 readwrite_gpg($answer."\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT, nocloseinput => 1);
847 };
848
849 return $signed_by_me;
850 };
851
852
853
854 my $USER;
855 my @KEYIDS;
856 my $params;
857
858 Getopt::Long::config('bundling');
859 if (!GetOptions (
860 '-h' => \$params->{'help'},
861 '--help' => \$params->{'help'},
862 '--version' => \$params->{'version'},
863 '-V' => \$params->{'version'},
864 '-u=s' => \$params->{'local-user'},
865 '--local-user=s' => \$params->{'local-user'},
866 '-e' => \$params->{'export-old'},
867 '--export-old' => \$params->{'export-old'},
868 '-E' => \$params->{'no-export-old'},
869 '--no-export-old' => \$params->{'no-export-old'},
870 '-m' => \$params->{'mail'},
871 '--mail' => \$params->{'mail'},
872 '-M' => \$params->{'no-mail'},
873 '--no-mail' => \$params->{'no-mail'},
874 '-R' => \$params->{'no-download'},
875 '--no-download' => \$params->{'no-download'},
876 '-S' => \$params->{'no-sign'},
877 '--no-sign' => \$params->{'no-sign'},
878 '--key-file=s@' => \$params->{'key-files'},
879 )) {
880 usage(\*STDERR, 1);
881 };
882 if ($params->{'help'}) {
883 usage(\*STDOUT, 0);
884 };
885 if ($params->{'version'}) {
886 version(\*STDOUT);
887 exit(0);
888 };
889 usage(\*STDERR, 1) unless scalar @ARGV >= 1;
890
891
892
893 if ($params->{'local-user'}) {
894 $USER = $params->{'local-user'};
895 $USER =~ s/^0x//i;
896 unless ($USER =~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
897 print STDERR "-u $USER is not a keyid.\n";
898 usage(\*STDERR, 1);
899 };
900 $USER = uc($USER);
901 };
902
903 for my $keyid (@ARGV) {
904 $keyid =~ s/^0x//i;
905 unless ($keyid =~ /^([A-F0-9]{8}|[A-F0-9]{16}||[A-F0-9]{40})$/i) {
906 if ($keyid =~ /^[A-F0-9]{32}$/) {
907 info("Ignoring v3 fingerprint $keyid. v3 keys are obsolete.");
908 next;
909 };
910 print STDERR "$keyid is not a keyid.\n";
911 usage(\*STDERR, 1);
912 };
913 push @KEYIDS, uc($keyid);
914 };
915
916 $CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
917 $CONFIG{'no-mail'} = $params->{'no-mail'} if defined $params->{'no-mail'};
918 $CONFIG{'mail'} = $params->{'mail'} if defined $params->{'mail'};
919 $CONFIG{'no-sign'} = $params->{'no-sign'} if defined $params->{'no-sign'};
920 push @{$CONFIG{'key-files'}}, @{$params->{'key-files'}} if defined $params->{'key-files'};
921
922
923 #################
924 # import own keys
925 #################
926 for my $keyid (@{$CONFIG{'keyid'}}) {
927 my $gpg = GnuPG::Interface->new();
928 $gpg->call( $CONFIG{'gpg'} );
929 $gpg->options->hash_init(
930 'homedir' => $GNUPGHOME,
931 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode --fast-list-mode } ] );
932 $gpg->options->meta_interactive( 0 );
933 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
934 my $pid = $gpg->list_public_keys(handles => $handles, command_args => $keyid);
935 my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
936 waitpid $pid, 0;
937
938 if ($stdout eq '') {
939 warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
940 };
941 unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
942 info("Key $keyid not found in caff's home. Getting it from your normal GnuPGHome.");
943 my $key = export_key(undef, $keyid);
944 if (!defined $key || $key eq '') {
945 warn ("Did not get key $keyid from your normal GnuPGHome\n");
946 next;
947 };
948 my $result = import_key($GNUPGHOME, $key);
949 unless ($result) {
950 warn ("Could not import $keyid into caff's gnupghome.\n");
951 next;
952 };
953 }
954 }
955
956 ########################
957 # import keys from files
958 ########################
959 foreach my $keyfile (@{$CONFIG{'key-files'}}) {
960 my $gpg = GnuPG::Interface->new();
961 $gpg->call( $CONFIG{'gpg'} );
962 $gpg->options->hash_init('homedir' => $GNUPGHOME);
963 $gpg->options->meta_interactive( 0 );
964 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
965 my $pid = $gpg->import_keys(handles => $handles, command_args => $keyfile);
966 my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
967 info ("Importing keys from $keyfile");
968 waitpid $pid, 0;
969 if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
970 warn $stderr;
971 }
972 }
973
974 #############################
975 # receive keys from keyserver
976 #############################
977 my @keyids_ok;
978 if ($CONFIG{'no-download'}) {
979 @keyids_ok = @KEYIDS;
980 } else {
981 info ("fetching keys, this will take a while...");
982
983 my $gpg = GnuPG::Interface->new();
984 $gpg->call( $CONFIG{'gpg'} );
985 $gpg->options->hash_init(
986 'homedir' => $GNUPGHOME,
987 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always }, '--keyserver='.$CONFIG{'keyserver'} ] );
988 $gpg->options->meta_interactive( 0 );
989 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
990 my $pid = $gpg->recv_keys(handles => $handles, command_args => [ @KEYIDS ]);
991 my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
992 waitpid $pid, 0;
993
994 # [GNUPG:] IMPORT_OK 0 5B00C96D5D54AEE1206BAF84DE7AAF6E94C09C7F
995 # [GNUPG:] NODATA 1
996 # [GNUPG:] NODATA 1
997 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
998 my %local_keyids = map { $_ => 1 } @KEYIDS;
999 my $had_v3_keys = 0;
1000 for my $line (split /\n/, $status) {
1001 if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
1002 my $imported_key = $1;
1003 my $whole_fpr = $imported_key;
1004 my $long_keyid = substr($imported_key, -16);
1005 my $short_keyid = substr($imported_key, -8);
1006 my $speced_key;
1007 for my $spec (($whole_fpr, $long_keyid, $short_keyid)) {
1008 $speced_key = $spec if $local_keyids{$spec};
1009 };
1010 unless ($speced_key) {
1011 notice ("Imported unexpected key; got: $imported_key\n");
1012 next;
1013 };
1014 debug ("Imported $imported_key for $speced_key");
1015 delete $local_keyids{$speced_key};
1016 unshift @keyids_ok, $imported_key;
1017 } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) {
1018 } elsif ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{32})/) {
1019 my $imported_key = $1;
1020 notice ("Imported key $1 is a version 3 key. Version 3 keys are obsolete, should not be used, and are not and will not be properly supported.");
1021 $had_v3_keys = 1;
1022 } else {
1023 notice ("got unknown reply from gpg: $line");
1024 }
1025 };
1026 if (scalar %local_keyids) {
1027 notice ("Import failed for: ". (join ' ', keys %local_keyids)."." . ($had_v3_keys ? " (Or maybe it's one of those ugly v3 keys?)" : ""));
1028 exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
1029 if (scalar %local_keyids == 1) {
1030 mywarn("Assuming ". (join ' ', keys %local_keyids)." is a fine keyid.");
1031 } else {
1032 mywarn("Assuming ". (join ' ', keys %local_keyids)." are fine keyids.");
1033 };
1034 push @keyids_ok, keys %local_keyids;
1035 }
1036 };
1037
1038 unless (@keyids_ok) {
1039 notice ("No keys to sign found");
1040 exit 0;
1041 }
1042
1043 ###########
1044 # sign keys
1045 ###########
1046 if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) {
1047 $CONFIG{'no-sign'} = ! ask("Continue with signing?", 1);
1048 }
1049
1050 unless ($CONFIG{'no-sign'}) {
1051 info("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
1052 for my $keyid (@keyids_ok) {
1053 my @command;
1054 push @command, $CONFIG{'gpg-sign'};
1055 push @command, '--local-user', $USER if (defined $USER);
1056 push @command, "--homedir=$GNUPGHOME";
1057 push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
1058 push @command, '--no-auto-check-trustdb';
1059 push @command, '--trust-model=always';
1060 push @command, '--edit', $keyid;
1061 push @command, 'sign';
1062 push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
1063 print join(' ', @command),"\n";
1064 system (@command);
1065 };
1066 };
1067
1068 ##################
1069 # export and prune
1070 ##################
1071 KEYS:
1072 for my $keyid (@keyids_ok) {
1073 # get key listing
1074 #################
1075 my $gpg = GnuPG::Interface->new();
1076 $gpg->call( $CONFIG{'gpg'} );
1077 $gpg->options->hash_init(
1078 'homedir' => $GNUPGHOME,
1079 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode } ] );
1080 $gpg->options->meta_interactive( 0 );
1081 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
1082 my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $keyid ]);
1083 my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
1084 waitpid $pid, 0;
1085 if ($stdout eq '') {
1086 warn ("No data from gpg for list-key $keyid\n");
1087 next;
1088 };
1089 my @publine = grep { /^pub/ } (split /\n/, $stdout);
1090 if (scalar @publine == 0) {
1091 warn ("No public keys found with list-key $keyid (note that caff uses its own keyring in $GNUPGHOME).\n");
1092 next;
1093 };
1094 my (undef, undef, undef, undef, $longkeyid, undef, undef, undef, undef, undef, undef, $flags) = split /:/, pop @publine;
1095 if (scalar @publine > 0) {
1096 warn ("More than one key matched $keyid. Try to specify the long keyid or fingerprint\n");
1097 next;
1098 };
1099 unless (defined $longkeyid) {
1100 warn ("Didn't find public keyid in --list-key of key $keyid.\n");
1101 next;
1102 };
1103 unless (defined $flags) {
1104 warn ("Didn't find flags in --list-key of key $keyid.\n");
1105 next;
1106 };
1107 my $can_encrypt = $flags =~ /E/;
1108
1109 # export the key
1110 ################
1111 my $asciikey = export_key($GNUPGHOME, $keyid);
1112 if ($asciikey eq '') {
1113 warn ("No data from gpg for export $keyid\n");
1114 next;
1115 };
1116
1117 my @UIDS;
1118 my $uid_number = 0;
1119 while (1) {
1120 my $this_uid_text = '';
1121 $uid_number++;
1122 debug("Doing key $keyid, uid $uid_number");
1123 my $tempdir = tempdir( "caff-$keyid-XXXXX", DIR => '/tmp/', CLEANUP => 1);
1124
1125 # import into temporary gpghome
1126 ###############################
1127 my $result = import_key($tempdir, $asciikey);
1128 unless ($result) {
1129 warn ("Could not import $keyid into temporary gnupg.\n");
1130 next;
1131 };
1132
1133 # prune it
1134 ##########
1135 $gpg = GnuPG::Interface->new();
1136 $gpg->call( $CONFIG{'gpg-delsig'} );
1137 $gpg->options->hash_init(
1138 'homedir' => $tempdir,
1139 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode --command-fd=0 --no-tty } ] );
1140 ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
1141 $pid = $gpg->wrap_call(
1142 commands => [ '--edit' ],
1143 command_args => [ $keyid ],
1144 handles => $handles );
1145
1146 debug("Starting edit session");
1147 ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1148
1149 # delete other uids
1150 ###################
1151 my $number_of_subkeys = 0;
1152 my $i = 1;
1153 my $have_one = 0;
1154 my $is_uat = 0;
1155 my $delete_some = 0;
1156 debug("Parsing stdout output.");
1157 for my $line (split /\n/, $stdout) {
1158 debug("Checking line $line");
1159 my ($type, undef, undef, undef, undef, undef, undef, undef, undef, $uidtext) = split /:/, $line;
1160 if ($type eq 'sub') {
1161 $number_of_subkeys++;
1162 };
1163 next unless ($type eq 'uid' || $type eq 'uat');
1164 debug("line is interesting.");
1165 if ($uid_number != $i) {
1166 debug("mark for deletion.");
1167 readwrite_gpg("$i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1168 $delete_some++;
1169 } else {
1170 debug("keep it.");
1171 $have_one = 1;
1172 $this_uid_text = ($type eq 'uid') ? $uidtext : '[attribute]';
1173 $is_uat = $type eq 'uat';
1174 };
1175 $i++;
1176 };
1177 debug("Parsing stdout output done.");
1178 unless ($have_one) {
1179 debug("Uid ".($uid_number-1)." was the last, there is no $uid_number.");
1180 info("key $keyid done.");
1181 last;
1182 };
1183
1184 my $prune_some_sigs_on_uid;
1185 my $prune_all_sigs_on_uid;
1186 if ($is_uat) {
1187 debug("handling attribute userid of key $keyid.");
1188 if ($uid_number == 1) {
1189 debug(" attribute userid is #1, unmarking #2 for deletion.");
1190 readwrite_gpg("2\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1191 $delete_some--;
1192 $prune_some_sigs_on_uid = 1;
1193 $prune_all_sigs_on_uid = 2;
1194 } else {
1195 debug("attribute userid is not #1, unmarking #1 for deletion.");
1196 readwrite_gpg("1\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1197 $delete_some--;
1198 $prune_some_sigs_on_uid = 2;
1199 $prune_all_sigs_on_uid = 1;
1200 };
1201 } else {
1202 $prune_some_sigs_on_uid = 1;
1203 };
1204
1205 if ($delete_some) {
1206 debug("need to delete $delete_some uids.");
1207 readwrite_gpg("deluid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_DELUID_PROMPT, nocloseinput => 1);
1208 readwrite_gpg("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1209 };
1210
1211 # delete subkeys
1212 ################
1213 if ($number_of_subkeys > 0) {
1214 for (my $i=1; $i<=$number_of_subkeys; $i++) {
1215 readwrite_gpg("key $i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1216 };
1217 readwrite_gpg("delkey\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_DELSUBKEY_PROMPT, nocloseinput => 1);
1218 readwrite_gpg("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
1219 };
1220
1221 # delete signatures
1222 ###################
1223 readwrite_gpg("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # mark uid for delsig
1224 my $signed_by_me = delete_signatures($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $CONFIG{'keyid'});
1225 readwrite_gpg("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # unmark uid from delsig
1226 if (defined $prune_all_sigs_on_uid) {
1227 readwrite_gpg("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # mark uid for delsig
1228 delete_signatures($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, []);
1229 readwrite_gpg("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # unmark uid from delsig
1230 };
1231
1232
1233 readwrite_gpg("save\n", $inputfd, $stdoutfd, $stderrfd, $statusfd);
1234 waitpid $pid, 0;
1235
1236 my $asciikey = export_key($tempdir, $keyid);
1237 if ($asciikey eq '') {
1238 warn ("No data from gpg for export $keyid\n");
1239 next;
1240 };
1241
1242 if ($signed_by_me) {
1243 if ($NOW - $signed_by_me > $CONFIG{'export-sig-age'} ) {
1244 my $write = ask("Signature on $this_uid_text is old. Export?", 0, $params->{'export-old'}, $params->{'no-export-old'});
1245 next unless $write;
1246 };
1247 my $keydir = "$KEYSBASE/$DATE_STRING";
1248 -d $keydir || mkpath($keydir , 0, 0700) or die ("Cannot create $keydir $!\n");
1249
1250 my $keyfile = "$keydir/$longkeyid.key.$uid_number.".sanitize_uid($this_uid_text).".asc";
1251 open (KEY, ">$keyfile") or die ("Cannot open $keyfile: $!\n");
1252 print KEY $asciikey;
1253 close KEY;
1254
1255 push @UIDS, { text => $this_uid_text, key => $asciikey, serial => $uid_number, "is_uat" => $is_uat };
1256
1257 info("$longkeyid $uid_number $this_uid_text done.");
1258 } else {
1259 info("$longkeyid $uid_number $this_uid_text is not signed by me, not writing.");
1260 };
1261 };
1262
1263 if (scalar @UIDS == 0) {
1264 info("found no signed uids for $keyid");
1265 } else {
1266 next if $CONFIG{'no-mail'}; # do not send mail
1267
1268 my @attached;
1269 for my $uid (@UIDS) {
1270 trace("UID: $uid->{'text'}\n");
1271 if ($uid->{'is_uat'}) {
1272 my $attach = ask("UID $uid->{'text'} is an attribute UID, attach it to every email sent?", 1);
1273 push @attached, $uid if $attach;
1274 } elsif ($uid->{'text'} !~ /@/) {
1275 my $attach = ask("UID $uid->{'text'} is no email address, attach it to every email sent?", 1);
1276 push @attached, $uid if $attach;
1277 };
1278 };
1279
1280 notice("Key has no encryption capabilities, mail will be sent unencrypted") unless $can_encrypt;
1281 for my $uid (@UIDS) {
1282 if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
1283 my $address = $uid->{'text'};
1284 $address =~ s/.*<(.*)>.*/$1/;
1285 if (ask("Mail signature for $uid->{'text'} to '$address'?", 1, $CONFIG{'mail'})) {
1286 my $mail = send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
1287
1288 my $keydir = "$KEYSBASE/$DATE_STRING";
1289 my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid($uid->{'text'});
1290 open (KEY, ">$mailfile") or die ("Cannot open $mailfile: $!\n");
1291 print KEY $mail;
1292 close KEY;
1293 };
1294 };
1295 };
1296 };
1297
1298 };