3 # caff -- CA - Fire and Forget
6 # Copyright (c) 2004, 2005 Peter Palfrader <peter@palfrader.org>
10 # Redistribution and use in source and binary forms, with or without
11 # modification, are permitted provided that the following conditions
13 # 1. Redistributions of source code must retain the above copyright
14 # notice, this list of conditions and the following disclaimer.
15 # 2. Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in the
17 # documentation and/or other materials provided with the distribution.
18 # 3. The name of the author may not be used to endorse or promote products
19 # derived from this software without specific prior written permission.
21 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 caff -- CA - Fire and Forget
42 =item B<caff> [-mMR] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
48 CA Fire and Forget is a script that helps you in keysigning. It takes a list
49 of keyids on the command line, fetches them from a keyserver and calls GnuPG so
50 that you can sign it. It then mails each key to all its email addresses - only
51 including the one UID that we send to in each mail, pruned from all but self
52 sigs and sigs done by you.
58 =item B<-m>, B<--mail>
60 Send mail after signing. Default is to ask the user for each uid.
62 =item B<-M>, B<--no-mail>
64 Do not send mail after signing. Default is to ask the user for each uid.
66 =item B<-R>, B<--no-download>
68 Do not retrieve the key to be signed from a keyserver.
70 =item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
72 Select the key that is used for signing, in case you have more than one key.
80 =item $HOME/.caffrc - configuration file
84 =head1 CONFIGURATION FILE OPTIONS
86 The configuration file is a perl script that sets values in the hash B<%CONFIG>.
90 $CONFIG{owner} = q{Peter Palfrader};
91 $CONFIG{email} = q{peter@palfrader.org};
92 $CONFIG{keyid} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
98 =item B<caffhome> [string]
100 Base directory for the files caff stores. Default: B<$HOME/.caff/>.
102 =item B<owner> [string]
104 Your name. B<REQUIRED>.
106 =item B<email> [string]
108 Your email address, used in From: lines. B<REQUIRED>.
110 =item B<keyid> [list of keyids]
112 A list of your keys. This is used to determine which signatures to keep
113 in the pruning step. If you select a key using B<-u> it has to be in
114 this list. B<REQUIRED>.
116 =item B<export-sig-age> [seconds]
118 Don't export UIDs by default, on which your latest signature is older
119 than this age. Default: B<24*60*60> (i.e. one day).
121 =item B<keyserver> [string]
123 Keyserver to download keys from. Default: B<subkeys.pgp.net>.
125 =item B<gpg> [string]
127 Path to the GnuPG binary. Default: B<gpg>.
129 =item B<gpg-sign> [string]
131 Path to the GnuPG binary which is used to sign keys. Default: what
134 =item B<gpg-delsig> [string]
136 Path to the GnuPG binary which is used to split off signatures. This was
137 needed while the upstream GnuPG was not fixed. Default: what B<gpg>
140 =item B<secret-keyring> [string]
142 Path to your secret keyring. Default: B<$HOME/.gnupg/secring.gpg>.
144 =item B<also-encrypt-to> [keyid]
146 An additional keyid to encrypt messages to. Default: none.
148 =item B<no-download> [boolean]
150 If true, then skip the step of fetching keys from the keyserver.
153 =item B<no-sign> [boolean]
155 If true, then skip the signing step. Default: B<0>.
157 =item B<mail-template> [string]
159 Email template which is used as the body text for the email sent out.
160 instead of the default text if specified. The following perl variables
161 can be used in the template:
165 =item B<{owner}> [string]
167 Your name as specified in the L<B<owner>|/item_owner__5bstring_5d> setting.
169 =item B<{key}> [string]
171 The keyid of the key you signed.
173 =item B<{@uids}> [array]
175 The UIDs for which signatures are included in the mail.
179 =item B<bcc> [string]
181 Address to send blind carbon copies to when sending mail.
188 Peter Palfrader <peter@palfrader.org>
192 http://pgp-tools.alioth.debian.org/
200 use File
::Temp
qw{tempdir
};
206 use GnuPG
::Interface
;
209 my $REVISION = '$Rev$';
210 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
211 my $VERSION = "0.0.0.$REVISION_NUMER";
214 my $config = $ENV{'HOME'} . '/.caffrc';
215 -f
$config or die "No file $config present. See caff(1).\n";
216 unless (scalar eval `cat $config`) {
217 die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
220 $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'};
221 die ("owner is not defined.\n") unless defined $CONFIG{'owner'};
222 die ("email is not defined.\n") unless defined $CONFIG{'email'};
223 die ("keyid is not defined.\n") unless defined $CONFIG{'keyid'};
224 die ("keyid is not an array ref\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
225 for my $keyid (@
{$CONFIG{'keyid'}}) {
226 $keyid =~ /^[A-Fa-z0-9]{16}$/ or die ("key $keyid is not a long (16 digit) keyid.\n");
228 @
{$CONFIG{'keyid'}} = map { uc } @
{$CONFIG{'keyid'}};
229 $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
230 $CONFIG{'keyserver'} = 'subkeys.pgp.net' unless defined $CONFIG{'keyserver'};
231 $CONFIG{'gpg'} = 'gpg' unless defined $CONFIG{'gpg'};
232 $CONFIG{'gpg-sign'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-sign'};
233 $CONFIG{'gpg-delsig'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-delsig'};
234 $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
235 $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
236 $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
237 $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
240 please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
241 {foreach $uid (@uids) {
242 $OUT .= "\t".$uid."\n";
243 };} of your key {$key} signed by me.
245 Note that I did not upload your key to any keyservers. If you want this
246 new signature to be available to others, please upload it yourself.
247 With GnuPG this can be done using
248 gpg --keyserver subkeys.pgp.net --send-key {$key}
250 If you have any questions, don't hesitate to ask.
259 print "[NOTICE] $line\n";
263 print "[INFO] $line\n";
267 #print "[DEBUG] $line\n";
271 #print "[trace] $line\n";
275 #print "[trace2] $line\n";
280 stdin
=> IO
::Handle
->new(),
281 stdout
=> IO
::Handle
->new(),
282 stderr
=> IO
::Handle
->new(),
283 status
=> IO
::Handle
->new() );
284 my $handles = GnuPG
::Handles
->new( %fds );
285 return ($fds{'stdin'}, $fds{'stdout'}, $fds{'stderr'}, $fds{'status'}, $handles);
288 sub readwrite_gpg
($$$$$%) {
289 my ($in, $inputfd, $stdoutfd, $stderrfd, $statusfd, %options) = @_;
291 trace
("Entering readwrite_gpg.");
293 my ($first_line, undef) = split /\n/, $in;
294 debug
("readwrite_gpg sends ".(defined $first_line ?
$first_line : "<nothing>"));
296 local $INPUT_RECORD_SEPARATOR = undef;
297 my $sout = IO
::Select
->new();
298 my $sin = IO
::Select
->new();
301 trace
("input is $inputfd; output is $stdoutfd; err is $stderrfd; status is ".(defined $statusfd ?
$statusfd : 'undef').".");
303 $inputfd->blocking(0);
304 $stdoutfd->blocking(0);
305 $statusfd->blocking(0) if defined $statusfd;
306 $stderrfd->blocking(0);
307 $sout->add($stdoutfd);
308 $sout->add($stderrfd);
309 $sout->add($statusfd) if defined $statusfd;
312 my ($stdout, $stderr, $status) = ("", "", "");
313 my $exitwhenstatusmatches = $options{'exitwhenstatusmatches'};
314 trace
("doing stuff until we find $exitwhenstatusmatches") if defined $exitwhenstatusmatches;
316 my $readwrote_stuff_this_time = 0;
317 my $do_not_wait_on_select = 0;
318 my ($readyr, $readyw, $written);
319 while ($sout->count() > 0 || (defined($sin) && ($sin->count() > 0))) {
320 if (defined $exitwhenstatusmatches) {
321 if ($status =~ /$exitwhenstatusmatches/m) {
322 trace
("readwrite_gpg found match on $exitwhenstatusmatches");
323 if ($readwrote_stuff_this_time) {
324 trace
("read/write some more\n");
325 $do_not_wait_on_select = 1;
327 trace
("that's it in our while loop.\n");
333 $readwrote_stuff_this_time = 0;
334 trace
("select waiting for ".($sout->count())." fds.");
335 ($readyr, $readyw, undef) = IO
::Select
::select($sout, $sin, undef, $do_not_wait_on_select ?
0 : 1);
336 trace
("ready: write: ".(defined $readyw ?
scalar @
$readyw : 0 )."; read: ".(defined $readyr ?
scalar @
$readyr : 0));
337 for my $wfd (@
$readyw) {
338 $readwrote_stuff_this_time = 1;
339 if (length($in) != $offset) {
340 trace
("writing to $wfd.");
341 $written = $wfd->syswrite($in, length($in) - $offset, $offset);
344 if ($offset == length($in)) {
345 trace
("writing to $wfd done.");
346 unless ($options{'nocloseinput'}) {
348 trace
("$wfd closed.");
355 next unless (defined(@
$readyr)); # Wait some more.
357 for my $rfd (@
$readyr) {
358 $readwrote_stuff_this_time = 1;
360 trace
("reading from $rfd done.");
365 trace
("reading from $rfd.");
366 if ($rfd == $stdoutfd) {
368 trace2
("stdout is now $stdout\n================");
371 if (defined $statusfd && $rfd == $statusfd) {
373 trace2
("status is now $status\n================");
376 if ($rfd == $stderrfd) {
378 trace2
("stderr is now $stderr\n================");
383 trace
("readwrite_gpg done.");
384 return ($stdout, $stderr, $status);
388 my ($question, $default) = @_;
391 print $question,' ',($default ?
'[Y/n]' : '[y/N]'), ' ';
394 last if ((defined $answer) && (length $answer <= 1));
398 my $result = $default;
399 $result = 1 if $answer =~ /y/i;
400 $result = 0 if $answer =~ /n/i;
408 my $KEYEDIT_PROMPT = '^\[GNUPG:\] GET_LINE keyedit.prompt';
409 my $KEYEDIT_DELUID_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.uid.okay';
410 my $KEYEDIT_DELSIG_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.delsig';
411 my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = '^\[GNUPG:\] (GET_BOOL keyedit.delsig|GET_LINE keyedit.prompt)';
412 my $KEYEDIT_DELSUBKEY_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.subkey';
415 my $USER_AGENT = "caff $VERSION - (c) 2004, 2005 Peter Palfrader";
417 my $KEYSBASE = $CONFIG{'caffhome'}.'/keys';
418 my $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
420 -d
$KEYSBASE || mkpath
($KEYSBASE , 0, 0700) or die ("Cannot create $KEYSBASE: $!\n");
421 -d
$GNUPGHOME || mkpath
($GNUPGHOME, 0, 0700) or die ("Cannot create $GNUPGHOME: $!\n");
424 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($NOW);
425 my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday);
430 print $fd "caff $VERSION - (c) 2004, 2005 Peter Palfrader\n";
434 my ($fd, $exitcode) = @_;
436 print $fd "Usage: $PROGRAM_NAME [-mMR] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
437 print $fd "Consult the manual page for more information.\n";
442 # export key $keyid from $gnupghome
445 my ($gnupghome, $keyid) = @_;
447 my $gpg = GnuPG
::Interface
->new();
448 $gpg->call( $CONFIG{'gpg'} );
449 $gpg->options->hash_init(
450 'homedir' => $gnupghome,
452 $gpg->options->meta_interactive( 0 );
453 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
454 my $pid = $gpg->export_keys(handles
=> $handles, command_args
=> [ $keyid ]);
455 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
462 # import a key from the scalar $asciikey into a gpg homedirectory in $tempdir
465 my ($gnupghome, $asciikey) = @_;
467 my $gpg = GnuPG
::Interface
->new();
468 $gpg->call( $CONFIG{'gpg'} );
469 $gpg->options->hash_init( 'homedir' => $gnupghome );
470 $gpg->options->meta_interactive( 0 );
471 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
472 my $pid = $gpg->import_keys(handles
=> $handles);
473 my ($stdout, $stderr, $status) = readwrite_gpg
($asciikey, $inputfd, $stdoutfd, $stderrfd, $statusfd);
476 if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
484 # Send an email to $address. If $can_encrypt is true then the mail
485 # will be PGP/MIME encrypted to $longkeyid.
487 # $longkeyid, $uid, and @attached will be used in the email and the template.
489 #send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
490 sub send_mail
($$$@
) {
491 my ($address, $can_encrypt, $key_id, @keys) = @_;
493 my $template = Text
::Template
->new(TYPE
=> 'STRING', SOURCE
=> $CONFIG{'mail-template'})
494 or die "Error creating template: $Text::Template::ERROR";
497 for my $key (@keys) {
498 push @uids, $key->{'text'};
500 my $message = $template->fill_in(HASH
=> { key
=> $key_id,
502 owner
=> $CONFIG{'owner'}})
503 or die "Error filling template in: $Text::Template::ERROR";
505 my $message_entity = MIME
::Entity
->build(
506 Type
=> "text/plain",
508 Disposition
=> 'inline',
512 for my $key (@keys) {
513 $message_entity->attach(
514 Type
=> "application/pgp-keys",
515 Disposition
=> 'attachment',
517 Description
=> "PGP Key 0x$key_id, uid ".($key->{'text'}).' ('.($key->{'serial'}).')',
518 Data
=> $key->{'key'},
519 Filename
=> "0x$key_id.".$key->{'serial'}.".asc");
523 my $message = $message_entity->stringify();
525 my $gpg = GnuPG
::Interface
->new();
526 $gpg->call( $CONFIG{'gpg'} );
527 $gpg->options->hash_init( 'homedir' => $GNUPGHOME,
528 'extra_args' => '--always-trust',
530 $gpg->options->meta_interactive( 0 );
531 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
532 $gpg->options->push_recipients( $key_id );
533 $gpg->options->push_recipients( $CONFIG{'also-encrypt-to'} ) if defined $CONFIG{'also-encrypt-to'};
534 my $pid = $gpg->encrypt(handles
=> $handles);
535 my ($stdout, $stderr, $status) = readwrite_gpg
($message, $inputfd, $stdoutfd, $stderrfd, $statusfd);
538 warn ("No data from gpg for list-key $key_id\n");
543 $message_entity = MIME
::Entity
->build(
544 Type
=> 'multipart/encrypted; protocol="application/pgp-encrypted"');
546 $message_entity->attach(
547 Type
=> "application/pgp-encrypted",
548 Disposition
=> 'attachment',
550 Data
=> "Version: 1\n");
552 $message_entity->attach(
553 Type
=> "application/octet-stream",
554 Filename
=> 'msg.asc',
555 Disposition
=> 'inline',
560 $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
561 $message_entity->head->add("To", $address);
562 $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
563 $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
564 $message_entity->head->add("User-Agent", $USER_AGENT);
565 $message_entity->send();
566 $message_entity->stringify();
570 # clean up a UID so that it can be used on the FS.
572 sub sanitize_uid
($) {
576 $good_uid =~ tr
#/:\\#_#;
577 trace2
("[sanitize_uid] changed UID from $uid to $good_uid.\n") if $good_uid ne $uid;
581 sub delete_signatures
($$$$$$) {
582 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $keyids) =@_;
584 my $signed_by_me = 0;
586 my ($stdout, $stderr, $status) =
587 readwrite_gpg
("delsig\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_DELSIG_PROMPT, nocloseinput
=> 1);
589 while($status =~ /$KEYEDIT_DELSIG_PROMPT/m) {
590 # sig:?::17:EA2199412477CAF8:1058095214:::::13x:
591 my @sigline = grep { /^sig/ } (split /\n/, $stdout);
592 $stdout =~ s/\n/\\n/g;
593 notice
("[sigremoval] why are there ".(scalar @sigline)." siglines in that part of the dialog!? got: $stdout") if scalar @sigline >= 2; # XXX
594 my $line = pop @sigline;
596 if (defined $line) { # only if we found a sig here - we never remove revocation packets for instance
597 debug
("[sigremoval] doing line $line.");
598 my (undef, undef, undef, undef, $signer, $created, undef, undef, undef) = split /:/, $line;
599 if ($signer eq $longkeyid) {
600 debug
("[sigremoval] selfsig ($signer).");
602 } elsif (grep { $signer eq $_ } @
{$keyids}) {
603 debug
("[sigremoval] signed by us ($signer).");
605 $signed_by_me = $signed_by_me > $created ?
$signed_by_me : $created;
607 debug
("[sigremoval] not interested in that sig ($signer).");
611 debug
("[sigremoval] no sig line here, only got: ".$stdout);
613 ($stdout, $stderr, $status) =
614 readwrite_gpg
($answer."\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT, nocloseinput
=> 1);
617 return $signed_by_me;
626 Getopt
::Long
::config
('bundling');
628 '-h' => \
$params->{'help'},
629 '--help' => \
$params->{'help'},
630 '--version' => \
$params->{'version'},
631 '-V' => \
$params->{'version'},
632 '-u=s' => \
$params->{'local-user'},
633 '--local-user=s' => \
$params->{'local-user'},
634 '-m' => \
$params->{'mail'},
635 '--mail' => \
$params->{'mail'},
636 '-M' => \
$params->{'no-mail'},
637 '--no-mail' => \
$params->{'no-mail'},
638 '-R' => \
$params->{'no-download'},
639 '--no-download' => \
$params->{'no-download'},
643 if ($params->{'help'}) {
646 if ($params->{'version'}) {
650 usage
(\
*STDERR
, 1) unless scalar @ARGV >= 1;
654 if ($params->{'local-user'}) {
655 $USER = $params->{'local-user'};
657 unless ($USER =~ /^[A-Za-z0-9]{8,8}([A-Za-z0-9]{8})?$/) {
658 print STDERR
"-u $USER is not a keyid.\n";
664 for my $keyid (@ARGV) {
666 unless ($keyid =~ /^[A-Za-z0-9]{8}([A-Za-z0-9]{8}|[A-Za-z0-9]{32})?$/) {
667 print STDERR
"$keyid is not a keyid.\n";
670 push @KEYIDS, uc($keyid);
673 $CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
674 $CONFIG{'no-mail'} = $params->{'no-mail'} if defined $params->{'no-mail'};
675 $CONFIG{'mail'} = $params->{'mail'} if defined $params->{'mail'};
681 my $gpg = GnuPG
::Interface
->new();
682 $gpg->call( $CONFIG{'gpg'} );
683 $gpg->options->hash_init(
684 'homedir' => $GNUPGHOME,
685 'extra_args' => '--keyserver='.$CONFIG{'keyserver'} );
686 $gpg->options->meta_interactive( 0 );
687 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
688 $gpg->options->hash_init( 'extra_args' => [ '--with-colons', '--fixed-list-mode' ] );
689 my $pid = $gpg->list_public_keys(handles
=> $handles, command_args
=> $CONFIG{'keyid'});
690 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
693 warn ("No data from gpg for list-key\n");
696 foreach my $keyid (@
{$CONFIG{'keyid'}}) {
697 unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
698 info
("Importing $keyid");
699 system "gpg --export $keyid | gpg --import --homedir $GNUPGHOME";
703 #############################
704 # receive keys from keyserver
705 #############################
708 if ($CONFIG{'no-download'}) {
709 @keyids_ok = @KEYIDS;
711 my $gpg = GnuPG
::Interface
->new();
712 $gpg->call( $CONFIG{'gpg'} );
713 $gpg->options->hash_init(
714 'homedir' => $GNUPGHOME,
715 'extra_args' => '--keyserver='.$CONFIG{'keyserver'} );
716 $gpg->options->meta_interactive( 0 );
717 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
719 my @local_keyids = @KEYIDS;
720 for my $keyid (@local_keyids) {
721 info
("fetching $keyid...");
722 my $pid = $gpg->recv_keys(handles
=> $handles, command_args
=> [ $keyid ]);
723 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
726 # [GNUPG:] IMPORT_OK 0 5B00C96D5D54AEE1206BAF84DE7AAF6E94C09C7F
729 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
731 for my $line (split /\n/, $status) {
732 if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
733 my $imported_key = $1;
734 if ($keyid ne $imported_key &&
735 $keyid ne substr($imported_key, -16) &&
736 $keyid ne substr($imported_key, -8)) {
737 warn("Imported unexpected key. expected: $keyid; got: $imported_key.\n");
740 push @keyids_ok, $keyid;
744 } elsif ($line =~ /^\[GNUPG:\] NODATA/) {
745 push @keyids_failed, $keyid;
752 notice
("Huh, what's up with $keyid?");
753 push @keyids_failed, $keyid;
757 die ("Still keys in \@KEYIDS. This should not happen.") if scalar @KEYIDS;
758 notice
("Import failed for: ". (join ' ', @keyids_failed).".") if scalar @keyids_failed;
764 unless ($CONFIG{'no-sign'}) {
765 info
("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
766 for my $keyid (@keyids_ok) {
768 push @command, $CONFIG{'gpg-sign'};
769 push @command, '--local-user', $USER if (defined $USER);
770 push @command, "--homedir=$GNUPGHOME";
771 push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
772 push @command, '--edit', $keyid;
773 push @command, 'sign';
774 print join(' ', @command),"\n";
783 for my $keyid (@keyids_ok) {
786 my $gpg = GnuPG
::Interface
->new();
787 $gpg->call( $CONFIG{'gpg'} );
788 $gpg->options->hash_init( 'homedir' => $GNUPGHOME );
789 $gpg->options->meta_interactive( 0 );
790 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
791 $gpg->options->hash_init( 'extra_args' => [ '--with-colons', '--fixed-list-mode' ] );
792 my $pid = $gpg->list_public_keys(handles
=> $handles, command_args
=> [ $keyid ]);
793 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
796 warn ("No data from gpg for list-key $keyid\n");
799 my @publine = grep { /^pub/ } (split /\n/, $stdout);
800 if (scalar @publine == 0) {
801 warn ("No public keys found with list-key $keyid (note that caff uses its own keyring in $GNUPGHOME).\n");
804 my (undef, undef, undef, undef, $longkeyid, undef, undef, undef, undef, undef, undef, $flags) = split /:/, pop @publine;
805 if (scalar @publine > 0) {
806 warn ("More than one key matched $keyid. Try to specify the long keyid or fingerprint\n");
809 unless (defined $longkeyid) {
810 warn ("Didn't find public keyid in --list-key of key $keyid.\n");
813 unless (defined $flags) {
814 warn ("Didn't find flags in --list-key of key $keyid.\n");
817 my $can_encrypt = $flags =~ /E/;
821 my $asciikey = export_key
($GNUPGHOME, $keyid);
822 if ($asciikey eq '') {
823 warn ("No data from gpg for export $keyid\n");
830 my $this_uid_text = '';
832 debug
("Doing key $keyid, uid $uid_number");
833 my $tempdir = tempdir
( "caff-$keyid-XXXXX", DIR
=> '/tmp/', CLEANUP
=> 1);
835 # import into temporary gpghome
836 ###############################
837 my $result = import_key
($tempdir, $asciikey);
839 warn ("Could not import $keyid into temporary gnupg.\n");
845 $gpg = GnuPG
::Interface
->new();
846 $gpg->call( $CONFIG{'gpg-delsig'} );
847 $gpg->options->hash_init(
848 'homedir' => $tempdir,
849 'extra_args' => [ '--with-colons', '--fixed-list-mode', '--command-fd=0', '--no-tty' ] );
850 ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
851 $pid = $gpg->wrap_call(
852 commands
=> [ '--edit' ],
853 command_args
=> [ $keyid ],
854 handles
=> $handles );
856 debug
("Starting edit session");
857 ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
861 my $number_of_subkeys = 0;
866 debug
("Parsing stdout output.");
867 for my $line (split /\n/, $stdout) {
868 debug
("Checking line $line");
869 my ($type, undef, undef, undef, undef, undef, undef, undef, undef, $uidtext) = split /:/, $line;
870 if ($type eq 'sub') {
871 $number_of_subkeys++;
873 next unless ($type eq 'uid' || $type eq 'uat');
874 debug
("line is interesting.");
875 if ($uid_number != $i) {
876 debug
("mark for deletion.");
877 readwrite_gpg
("$i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
882 $this_uid_text = ($type eq 'uid') ?
$uidtext : '[attribute]';
883 $is_uat = $type eq 'uat';
887 debug
("Parsing stdout output done.");
889 debug
("Uid ".($uid_number-1)." was the last, there is no $uid_number.");
890 info
("key $keyid done.");
894 my $prune_some_sigs_on_uid;
895 my $prune_all_sigs_on_uid;
897 debug
("handling attribute userid of key $keyid.");
898 if ($uid_number == 1) {
899 debug
(" attribute userid is #1, unmarking #2 for deletion.");
900 readwrite_gpg
("2\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
902 $prune_some_sigs_on_uid = 1;
903 $prune_all_sigs_on_uid = 2;
905 debug
("attribute userid is not #1, unmarking #1 for deletion.");
906 readwrite_gpg
("1\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
908 $prune_some_sigs_on_uid = 2;
909 $prune_all_sigs_on_uid = 1;
912 $prune_some_sigs_on_uid = 1;
916 debug
("need to delete $delete_some uids.");
917 readwrite_gpg
("deluid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_DELUID_PROMPT, nocloseinput
=> 1);
918 readwrite_gpg
("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
923 if ($number_of_subkeys > 0) {
924 for (my $i=1; $i<=$number_of_subkeys; $i++) {
925 readwrite_gpg
("key $i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
927 readwrite_gpg
("delkey\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_DELSUBKEY_PROMPT, nocloseinput
=> 1);
928 readwrite_gpg
("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
933 readwrite_gpg
("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # mark uid for delsig
934 my $signed_by_me = delete_signatures
($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $CONFIG{'keyid'});
935 readwrite_gpg
("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # unmark uid from delsig
936 if (defined $prune_all_sigs_on_uid) {
937 readwrite_gpg
("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # mark uid for delsig
938 delete_signatures
($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, []);
939 readwrite_gpg
("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # unmark uid from delsig
943 readwrite_gpg
("save\n", $inputfd, $stdoutfd, $stderrfd, $statusfd);
946 my $asciikey = export_key
($tempdir, $keyid);
947 if ($asciikey eq '') {
948 warn ("No data from gpg for export $keyid\n");
953 if ($NOW - $signed_by_me > $CONFIG{'export-sig-age'} ) {
954 my $write = ask
("Signature on $this_uid_text is old. Export?", 0);
957 my $keydir = "$KEYSBASE/$DATE_STRING";
958 -d
$keydir || mkpath
($keydir , 0, 0700) or die ("Cannot create $keydir $!\n");
960 my $keyfile = "$keydir/$longkeyid.key.$uid_number.".sanitize_uid
($this_uid_text).".asc";
961 open (KEY
, ">$keyfile") or die ("Cannot open $keyfile: $!\n");
965 push @UIDS, { text
=> $this_uid_text, key
=> $asciikey, serial
=> $uid_number, "is_uat" => $is_uat };
967 info
("$longkeyid $uid_number $this_uid_text done.");
969 info
("$longkeyid $uid_number $this_uid_text is not signed by me, not writing.");
973 if (scalar @UIDS == 0) {
974 info
("found no signed uids for $keyid");
976 next if $CONFIG{'no-mail'}; # do not send mail
979 for my $uid (@UIDS) {
980 trace
("UID: $uid->{'text'}\n");
981 if ($uid->{'is_uat'}) {
982 my $attach = ask
("UID $uid->{'text'} is an attribute UID, attach it to every email sent?", 1);
983 push @attached, $uid if $attach;
984 } elsif ($uid->{'text'} !~ /@/) {
985 my $attach = ask
("UID $uid->{'text'} is no email address, attach it to every email sent?", 1);
986 push @attached, $uid if $attach;
990 notice
("Key has no encryption capabilities, mail will be sent unencrypted") unless $can_encrypt;
991 for my $uid (@UIDS) {
992 if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
993 my $address = $uid->{'text'};
994 $address =~ s/.*<(.*)>.*/$1/;
995 if ($CONFIG{'mail'} or ask
("Send mail to '$address' for $uid->{'text'}?", 1)) {
996 my $mail = send_mail
($address, $can_encrypt, $longkeyid, $uid, @attached);
998 my $keydir = "$KEYSBASE/$DATE_STRING";
999 my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid
($uid->{'text'});
1000 open (KEY
, ">$mailfile") or die ("Cannot open $mailfile: $!\n");